Compare commits

...

99 commits

Author SHA1 Message Date
Joffrey F
aa8fb8f708 Merge pull request #4499 from xulike666/function-name-modification
function-name-modification for tests/*
2017-02-16 11:59:36 -08:00
Joffrey F
b49f42f9b7 Merge pull request #4496 from xulike666/quick-easy-typo-fix
fix a typo in script/release/utils.sh
2017-02-16 11:59:17 -08:00
Joffrey F
afd3bcfbf4 Merge pull request #4484 from shin-/4425-sys-path-fix
Reinitialize sys.path after it's been potentially modified by pip
2017-02-16 11:39:54 -08:00
Aaron.L.Xu
d20e3f3342 function-name-modification for tests/*
Signed-off-by: Aaron.L.Xu <likexu@harmonycloud.cn>
2017-02-16 15:25:20 +08:00
Aaron.L.Xu
27297fd1af fix a typo in script/release/utils.sh
Signed-off-by: Aaron.L.Xu <likexu@harmonycloud.cn>
2017-02-16 11:14:49 +08:00
Joffrey F
66f4a795a2 Don't import pip inside Compose
Signed-off-by: Joffrey F <joffrey@docker.com>
2017-02-15 16:31:50 -08:00
Joffrey F
40c26ca676 Merge pull request #4480 from shin-/4479-merge-secrets
Fix `config` command output with service.secrets section
2017-02-14 17:08:18 -08:00
Joffrey F
abce83ef25 Fix config command output with service.secrets section
Signed-off-by: Joffrey F <joffrey@docker.com>
2017-02-13 16:51:50 -08:00
Joffrey F
bb5d7b2433 Merge pull request #4473 from Vehsamrak/patch-1
Compose file reference link fix in README.md
2017-02-13 13:03:39 -08:00
Petr Karmashev
252699c1d1 Compose file reference link fix in README.md
Signed-off-by: Petr Karmashev <smonkl@bk.ru>
2017-02-12 02:10:34 +03:00
Joffrey F
ad0e6d219b Merge pull request #4469 from dnephin/fix_secrets_config
Fixes secrets config loading
2017-02-10 18:39:53 -08:00
Daniel Nephin
dc5b3f3b3e Fix secrets config.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2017-02-10 17:11:24 -05:00
Joffrey F
1fe2443735 Merge pull request #4438 from fate-grand-order/master
fix typo in CHANGELOG.md
2017-02-09 16:35:35 -08:00
Joffrey F
27e0f31275 Merge pull request #4443 from xulike666/fix-accessibility
Referencing the right segment of code in compose/bundle.py
2017-02-09 16:34:48 -08:00
Joffrey F
2f13201b9e Merge pull request #4453 from kevinetc123/patch-typo
fix typo in project.py
2017-02-09 16:33:07 -08:00
Joffrey F
276db7231a Merge pull request #4455 from dnephin/fix_3.1
Fix version 3.1
2017-02-09 12:38:29 -08:00
Daniel Nephin
c092fa37de Fix version 3.1
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2017-02-09 12:39:57 -05:00
kevinetc123
47e4442722 fix typo in project.py
Signed-off-by: kevinetc123 <kaiwentan@harmonycloud.cn>
2017-02-09 19:10:26 +08:00
Joffrey F
b306e843d3 Merge pull request #4448 from shin-/1.11-release
1.11 release-master realign
2017-02-08 13:54:12 -08:00
Joffrey F
fc7b74d7f9 Bump to next dev version
Signed-off-by: Joffrey F <joffrey@docker.com>
2017-02-08 13:47:08 -08:00
Joffrey F
2cd6cb9a47 Bump 1.10.1
Signed-off-by: Joffrey F <joffrey@docker.com>
2017-02-08 13:45:25 -08:00
Joffrey F
01d1895a35 Bump 1.11.0
Signed-off-by: Joffrey F <joffrey@docker.com>
2017-02-08 13:44:40 -08:00
Joffrey F
979a0d53f7 Bump 1.11.0-rc1
Signed-off-by: Joffrey F <joffrey@docker.com>
2017-02-08 13:44:31 -08:00
Aaron.L.Xu
f083526829 referencing right segment of code
Signed-off-by: Aaron.L.Xu <likexu@harmonycloud.cn>
2017-02-08 18:50:26 +08:00
fate-grand-order
b392b6e12e fix typo in CHANGELOG.md
Signed-off-by: fate-grand-order <chenjg@harmonycloud.cn>
2017-02-07 15:59:34 +08:00
Joffrey F
165eb9c91a Merge pull request #3558 from shin-/1135-pyinstaller-update
Use newer version of PyInstaller to fix prelinking issues
2017-02-06 15:00:39 -08:00
Joffrey F
1636985a7a Merge pull request #4426 from shin-/4392-context-mgr
Close the open file handle using context manager
2017-02-06 13:30:49 -08:00
Kevin Jing Qiu
a3a9d8944a Close the open file handle using context manager
Signed-off-by: Kevin Jing Qiu <kevin.qiu@points.com>
2017-02-03 14:50:40 -08:00
Joffrey F
951497c0f2 Merge pull request #4419 from shin-/4418-healthcheck-extends
Don't re-parse healthcheck values coming from extended services
2017-02-03 12:24:53 -08:00
Joffrey F
e22164ec9f Merge pull request #4035 from urda/urda/compose-top
Added `top` to `docker-compose` to display running processes
2017-02-02 15:41:14 -08:00
Joffrey F
f106d23776 Merge pull request #4414 from shin-/4184-merge-pids
Add missing comma in DOCKER_CONFIG_KEYS list
2017-02-02 14:51:42 -08:00
Joffrey F
cf43e6edf7 Don't re-parse healthcheck values coming from extended services
Signed-off-by: Joffrey F <joffrey@docker.com>
2017-02-02 14:50:53 -08:00
Joffrey F
7e8958e6ca Add missing comma in DOCKER_CONFIG_KEYS list
Signed-off-by: Joffrey F <joffrey@docker.com>
2017-02-01 16:40:43 -08:00
Joffrey F
11038c455b Merge pull request #4334 from muicoder/master
add IMAGE_EVENTS: load/save
2017-02-01 15:49:03 -08:00
Peter Urda
a67500ee57
Added top to docker-compose to display running processes.
This commit allows `docker-compose` to access `top` for containers
much like running `docker top` directly on a given container.

This commit includes:

* `docker-compose` CLI changes to expose `top`
* Completions for `bash` and `zsh`
* Required testing for the new `top` command

Signed-off-by: Peter Urda <peter.urda@gmail.com>
2017-02-01 15:42:30 -08:00
Joffrey F
1f39b33357 Merge pull request #3989 from mattjbray/patch-1
Zsh completion: permit multiple --file arguments
2017-02-01 15:08:39 -08:00
Joffrey F
67e1111806 Merge pull request #4410 from shin-/4408-colors-no-tty
Don't strip ANSI color codes when output is not a TTY
2017-02-01 14:20:10 -08:00
Joffrey F
84774cacd2 Upgrade python and pip versions in Dockerfile
Add libbz2 dependency

Signed-off-by: Joffrey F <joffrey@docker.com>
2017-02-01 14:12:41 -08:00
Joffrey F
c9eb9380ed Merge pull request #4368 from dnephin/secrets-using-bind-mounts
Secrets using bind mounts
2017-02-01 14:11:20 -08:00
Joffrey F
c16cd77737 Merge pull request #4406 from shin-/bump_docker_py
Bump docker SDK version
2017-01-31 15:36:30 -08:00
Joffrey F
8efb7e6e8b Don't strip ANSI color codes when output is not a TTY
Signed-off-by: Joffrey F <joffrey@docker.com>
2017-01-31 12:51:46 -08:00
Daniel Nephin
59d1847d9b Fix some test failures.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2017-01-31 09:53:40 -05:00
Daniel Nephin
3a2735abb9 Rebase compose v3.1 on the latest v3
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2017-01-31 09:53:16 -05:00
Daniel Nephin
0d609b68ac Add a warning for unsupported secret fields.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2017-01-31 09:53:16 -05:00
Daniel Nephin
4053adc7d3 Add an integration test for secrets using bind mounts.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2017-01-31 09:53:16 -05:00
Daniel Nephin
e0c6397999 Implement secrets using bind mounts
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2017-01-31 09:53:16 -05:00
Daniel Nephin
add56ce818 Read service secrets as a type.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2017-01-31 09:53:16 -05:00
Daniel Nephin
a82de8863e Add v3.1 with secrets.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2017-01-31 09:53:16 -05:00
Joffrey F
2593366a3e Bump docker SDK version
Signed-off-by: Joffrey F <joffrey@docker.com>
2017-01-30 16:49:05 -08:00
Joffrey F
22249add84 Use newer version of PyInstaller to fix prelinking issues
Signed-off-by: Joffrey F <joffrey@docker.com>
2017-01-30 13:52:27 -08:00
Joffrey F
76d4f5bea6 Merge pull request #4383 from shin-/4344-detect-docker-py
Detect conflicting version of the docker python SDK
2017-01-30 12:14:27 -08:00
Joffrey F
5895d8bbc9 Detect conflicting version of the docker python SDK and prevent execution
until issue is fixed

Signed-off-by: Joffrey F <joffrey@docker.com>
2017-01-26 17:12:41 -08:00
Joffrey F
e05a9f4e62 Merge pull request #4389 from shin-/4372-normalize-time-values
Convert time data back to string values when serializing config
2017-01-26 13:47:34 -08:00
Joffrey F
e10d1140b9 Convert time data back to string values when serializing config
Signed-off-by: Joffrey F <joffrey@docker.com>
2017-01-26 11:59:22 -08:00
Joffrey F
56e01f25ea Merge pull request #4367 from dnephin/add-missing-network-internal-to-v3
Add missing network.internal to v3 schema.
2017-01-25 13:41:41 -08:00
Joffrey F
c86faab4ec Merge pull request #4370 from shin-/4357-win32-unicode-paths
Don't encode build context path on Windows
2017-01-23 12:04:42 -08:00
Joffrey F
20d6f450b5 Don't encode build context path on Windows
Signed-off-by: Joffrey F <joffrey@docker.com>
2017-01-20 15:05:53 -08:00
Daniel Nephin
644e1716c3 Add missing network.internal to v3 schema.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2017-01-20 12:55:59 -05:00
Joffrey F
9a0962dacb Merge pull request #4361 from shin-/4348-serialize-ext-volumes
Remove external_name from volume def in config output
2017-01-19 17:41:08 -08:00
Joffrey F
263b9e9317 Merge pull request #4360 from shin-/4359-volume-labels
Fix volume definition in v3 schema
2017-01-19 17:40:29 -08:00
Joffrey F
d83d31889e Remove external_name from volume def in config output
Signed-off-by: Joffrey F <joffrey@docker.com>
2017-01-19 16:05:13 -08:00
Joffrey F
5c2165eaaf Fix volume definition in v3 schema
Signed-off-by: Joffrey F <joffrey@docker.com>
2017-01-19 15:41:31 -08:00
Joffrey F
8a27a0f059 Merge pull request #4350 from shin-/fix-invalid-depends-on-merge
depends_on merge now retains condition information when present
2017-01-19 14:50:29 -08:00
Joffrey F
b47c97e94e Merge pull request #4358 from shin-/1.11-dev
1.11.0dev
2017-01-19 14:50:10 -08:00
Joffrey F
a482c138d8 Merge pull request #4353 from xulike666/fight-for-readability
Fix typo in script/test/versions.py
2017-01-19 14:49:05 -08:00
Joffrey F
1c46525c2b 1.11.0dev
Signed-off-by: Joffrey F <joffrey@docker.com>
2017-01-19 14:47:31 -08:00
Aaron.L.Xu
169289c8b6 find a fishbone
Signed-off-by: Aaron.L.Xu <likexu@harmonycloud.cn>
2017-01-20 00:52:19 +08:00
Joffrey F
1a02121ab5 depends_on merge now retains condition information when present
Signed-off-by: Joffrey F <joffrey@docker.com>
2017-01-18 17:52:03 -08:00
Joffrey F
708c4f9534 Merge pull request #4339 from shin-/4328-catch-healthcheck-exception
Catch healthcheck exceptions in parallel_execute
2017-01-17 14:16:37 -08:00
Joffrey F
56a1b02aac Catch healthcheck exceptions in parallel_execute
Signed-off-by: Joffrey F <joffrey@docker.com>
2017-01-17 13:22:16 -08:00
muicoder
931027c598 add IMAGE_EVENTS: load/save
Signed-off-by: muicoder <muicoder@gmail.com>
2017-01-16 10:53:40 +08:00
Joffrey F
5ade097d74 Merge pull request #4324 from shin-/4321-v3-depends-on
Provide valid serialization of depends_on when format is not 2.1
2017-01-12 11:52:15 -08:00
Joffrey F
62cdd25b7d Merge pull request #4323 from shin-/fix-push-release-script
Use correct wheel file name in twine upload command
2017-01-12 11:52:05 -08:00
Joffrey F
2df31bb13c Provide valid serialization of depends_on when format is not 2.1
Signed-off-by: Joffrey F <joffrey@docker.com>
2017-01-11 16:25:40 -08:00
Joffrey F
29b46d5b26 Use correct wheel file name in twine upload command
Signed-off-by: Joffrey F <joffrey@docker.com>
2017-01-11 15:39:48 -08:00
Joffrey F
2091149fee Merge pull request #4317 from shin-/release-script-fixes
Fix docker image build script when using universal wheels
2017-01-10 17:21:27 -08:00
Joffrey F
19190ea0df Fix docker image build script when using universal wheels
Signed-off-by: Joffrey F <joffrey@docker.com>
2017-01-10 16:43:26 -08:00
Joffrey F
1c1fe89e43 Merge pull request #4316 from shin-/update-setup-py
Update setup.py extra_requires
2017-01-10 15:50:11 -08:00
Joffrey F
52792b7a96 Update setup.py extra_requires
Signed-off-by: Joffrey F <joffrey@docker.com>
2017-01-10 14:59:11 -08:00
Joffrey F
9f6778aa73 Merge pull request #4172 from graingert/enable-universal-wheels
enable universal wheels
2017-01-10 14:57:22 -08:00
Joffrey F
545153f117 Merge pull request #4288 from tntC4stl3/fix_404_issue
Fix 404 issue, change APIError to more accureate ImageNotFound
2017-01-09 16:43:57 -08:00
Joffrey F
3f7b3fbf0a Merge pull request #4304 from shin-/4302-dockerignore-windows
Use docker SDK patch
2017-01-09 16:14:57 -08:00
Joffrey F
88294b46dd Merge pull request #4294 from shin-/4240-compose-convert-false
Ensure falsy values in COMPOSE_CONVERT_WINDOWS_PATHS are properly recognized
2017-01-09 15:49:44 -08:00
Joffrey F
2c157e8fa9 Use docker SDK 2.0.1
Signed-off-by: Joffrey F <joffrey@docker.com>
2017-01-09 15:27:20 -08:00
Joffrey F
b570cba965 Merge pull request #3961 from ankon/patch-1
Fix typo
2017-01-09 12:15:04 -08:00
Joffrey F
3bb8a7d178 Merge pull request #4297 from shin-/4271-fix-schemas
Fix config schemas (misplaced "additionalProperties")
2017-01-06 12:10:22 -08:00
Joffrey F
45c7ee4466 Merge pull request #4279 from dnephin/fix-schema-typo
Fix schema typo
2017-01-05 17:15:39 -08:00
Joffrey F
e063c5739f Fix config schemas (misplaced "additionalProperties")
Signed-off-by: Joffrey F <joffrey@docker.com>
2017-01-05 11:18:21 -08:00
Joffrey F
534b4ed820 Falsy values in COMPOSE_CONVERT_WINDOWS_PATHS are properly recognized
Signed-off-by: Joffrey F <joffrey@docker.com>
2017-01-04 15:45:11 -08:00
Joffrey F
27d91bba01 Merge pull request #4293 from shin-/stop_timeout_v2.1
Add support for stop_grace_period in v2
2017-01-04 15:14:18 -08:00
Joffrey F
1be41f59c9 Add support for stop_grace_period in v2
Signed-off-by: Joffrey F <joffrey@docker.com>
2017-01-04 14:30:20 -08:00
Joffrey F
838bdd71f3 Merge pull request #4291 from shin-/unify-healthcheck-def-v2v3
Unify healthcheck spec definition in v2 and v3
2017-01-04 13:53:39 -08:00
Joffrey F
8145429399 Unify healthcheck spec definition in v2 and v3
Signed-off-by: Joffrey F <joffrey@docker.com>
2017-01-04 13:14:23 -08:00
Thomas Grainger
2648af6807
enable universal wheels
Signed-off-by: Thomas Grainger <tom.grainger@procensus.com>
2017-01-04 18:33:58 +00:00
Jun Guo
c73fc26824 Fix 404 issue, change APIError to more accureate ImageNotFound
Signed-off-by: Jun Guo <blackhumour.gj@gmail.com>
2017-01-04 15:42:31 +08:00
Daniel Nephin
a74b2f2f70 Fix schema typo.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-12-28 15:16:53 -05:00
Matt Bray
a37d99f201 Zsh completion: change --file description text
Signed-off-by: Matt Bray <mattjbray@gmail.com>
2016-09-30 00:45:46 +01:00
Matthew Bray
90356b7040 Zsh completion: permit multiple --file arguments
Before this change:

```
$ docker-compose --file docker-compose.yml -<TAB>
 -- option --
--help                 -h  -- Get help
--host                 -H  -- Daemon socket to connect to
--project-name         -p  -- Specify an alternate project name (default: directory name)
--skip-hostname-check      -- Don't check the daemon's hostname against the name specified in the client certificate (for example if your docker host is an IP address)
--tls                      -- Use TLS; implied by --tlsverify
--tlscacert                -- Trust certs signed only by this CA
--tlscert                  -- Path to TLS certificate file
--tlskey                   -- Path to TLS key file
--tlsverify                -- Use TLS and verify the remote
--verbose                  -- Show more output
--version              -v  -- Print version and exit
```

(Note the `--file` argument is no longer available to complete.)

After this change:

```
docker-compose --file docker-compose.yml -<TAB>
 -- option --
--file                 -f  -- Specify an alternate docker-compose file (default: docker-compose.yml)
--help                 -h  -- Get help
--host                 -H  -- Daemon socket to connect to
--project-name         -p  -- Specify an alternate project name (default: directory name)
--skip-hostname-check      -- Don't check the daemon's hostname against the name specified in the client certificate (for example if your docker host is an IP address)
--tls                      -- Use TLS; implied by --tlsverify
--tlscacert                -- Trust certs signed only by this CA
--tlscert                  -- Path to TLS certificate file
--tlskey                   -- Path to TLS key file
--tlsverify                -- Use TLS and verify the remote
--verbose                  -- Show more output
--version              -v  -- Print version and exit
```

Signed-off-by: Matt Bray <mattjbray@gmail.com>
2016-09-28 12:04:13 +01:00
Andreas Kohn
cb3bf869f4 Fix typo
Signed-off-by: Andreas Kohn <andreas.kohn@gmail.com>
2016-09-20 13:59:17 +02:00
48 changed files with 1495 additions and 194 deletions

View file

@ -1,6 +1,101 @@
Change log Change log
========== ==========
1.11.0 (2017-02-08)
-------------------
### New Features
#### Compose file version 3.1
- Introduced version 3.1 of the `docker-compose.yml` specification. This
version requires Docker Engine 1.13.0 or above. It introduces support
for secrets. See the documentation for more information
#### Compose file version 2.0 and up
- Introduced the `docker-compose top` command that displays processes running
for the different services managed by Compose.
### Bugfixes
- Fixed a bug where extending a service defining a healthcheck dictionary
would cause `docker-compose` to error out.
- Fixed an issue where the `pid` entry in a service definition was being
ignored when using multiple Compose files.
1.10.1 (2017-02-01)
------------------
### Bugfixes
- Fixed an issue where presence of older versions of the docker-py
package would cause unexpected crashes while running Compose
- Fixed an issue where healthcheck dependencies would be lost when
using multiple compose files for a project
- Fixed a few issues that made the output of the `config` command
invalid
- Fixed an issue where adding volume labels to v3 Compose files would
result in an error
- Fixed an issue on Windows where build context paths containing unicode
characters were being improperly encoded
- Fixed a bug where Compose would occasionally crash while streaming logs
when containers would stop or restart
1.10.0 (2017-01-18)
-------------------
### New Features
#### Compose file version 3.0
- Introduced version 3.0 of the `docker-compose.yml` specification. This
version requires to be used with Docker Engine 1.13 or above and is
specifically designed to work with the `docker stack` commands.
#### Compose file version 2.1 and up
- Healthcheck configuration can now be done in the service definition using
the `healthcheck` parameter
- Containers dependencies can now be set up to wait on positive healthchecks
when declared using `depends_on`. See the documentation for the updated
syntax.
**Note:** This feature will not be ported to version 3 Compose files.
- Added support for the `sysctls` parameter in service definitions
- Added support for the `userns_mode` parameter in service definitions
- Compose now adds identifying labels to networks and volumes it creates
#### Compose file version 2.0 and up
- Added support for the `stop_grace_period` option in service definitions.
### Bugfixes
- Colored output now works properly on Windows.
- Fixed a bug where docker-compose run would fail to set up link aliases
in interactive mode on Windows.
- Networks created by Compose are now always made attachable
(Compose files v2.1 and up).
- Fixed a bug where falsy values of `COMPOSE_CONVERT_WINDOWS_PATHS`
(`0`, `false`, empty value) were being interpreted as true.
- Fixed a bug where forward slashes in some .dockerignore patterns weren't
being parsed correctly on Windows
1.9.0 (2016-11-16) 1.9.0 (2016-11-16)
----------------- -----------------
@ -143,7 +238,7 @@ Bug Fixes
- Fixed a bug in Windows environment where volume mappings of the - Fixed a bug in Windows environment where volume mappings of the
host's root directory would be parsed incorrectly. host's root directory would be parsed incorrectly.
- Fixed a bug where `docker-compose config` would ouput an invalid - Fixed a bug where `docker-compose config` would output an invalid
Compose file if external networks were specified. Compose file if external networks were specified.
- Fixed an issue where unset buildargs would be assigned a string - Fixed an issue where unset buildargs would be assigned a string
@ -525,7 +620,7 @@ Bug Fixes:
if at least one container is using the network. if at least one container is using the network.
- When printings logs during `up` or `logs`, flush the output buffer after - When printings logs during `up` or `logs`, flush the output buffer after
each line to prevent buffering issues from hideing logs. each line to prevent buffering issues from hiding logs.
- Recreate a container if one of its dependencies is being created. - Recreate a container if one of its dependencies is being created.
Previously a container was only recreated if it's dependencies already Previously a container was only recreated if it's dependencies already

View file

@ -13,6 +13,7 @@ RUN set -ex; \
ca-certificates \ ca-certificates \
curl \ curl \
libsqlite3-dev \ libsqlite3-dev \
libbz2-dev \
; \ ; \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
@ -20,40 +21,32 @@ RUN curl https://get.docker.com/builds/Linux/x86_64/docker-1.8.3 \
-o /usr/local/bin/docker && \ -o /usr/local/bin/docker && \
chmod +x /usr/local/bin/docker chmod +x /usr/local/bin/docker
# Build Python 2.7.9 from source # Build Python 2.7.13 from source
RUN set -ex; \ RUN set -ex; \
curl -L https://www.python.org/ftp/python/2.7.9/Python-2.7.9.tgz | tar -xz; \ curl -L https://www.python.org/ftp/python/2.7.13/Python-2.7.13.tgz | tar -xz; \
cd Python-2.7.9; \ cd Python-2.7.13; \
./configure --enable-shared; \ ./configure --enable-shared; \
make; \ make; \
make install; \ make install; \
cd ..; \ cd ..; \
rm -rf /Python-2.7.9 rm -rf /Python-2.7.13
# Build python 3.4 from source # Build python 3.4 from source
RUN set -ex; \ RUN set -ex; \
curl -L https://www.python.org/ftp/python/3.4.3/Python-3.4.3.tgz | tar -xz; \ curl -L https://www.python.org/ftp/python/3.4.6/Python-3.4.6.tgz | tar -xz; \
cd Python-3.4.3; \ cd Python-3.4.6; \
./configure --enable-shared; \ ./configure --enable-shared; \
make; \ make; \
make install; \ make install; \
cd ..; \ cd ..; \
rm -rf /Python-3.4.3 rm -rf /Python-3.4.6
# Make libpython findable # Make libpython findable
ENV LD_LIBRARY_PATH /usr/local/lib ENV LD_LIBRARY_PATH /usr/local/lib
# Install setuptools
RUN set -ex; \
curl -L https://bootstrap.pypa.io/ez_setup.py | python
# Install pip # Install pip
RUN set -ex; \ RUN set -ex; \
curl -L https://pypi.python.org/packages/source/p/pip/pip-8.1.1.tar.gz | tar -xz; \ curl -L https://bootstrap.pypa.io/get-pip.py | python
cd pip-8.1.1; \
python setup.py install; \
cd ..; \
rm -rf pip-8.1.1
# Python3 requires a valid locale # Python3 requires a valid locale
RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && locale-gen RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && locale-gen

View file

@ -1,5 +1,6 @@
FROM alpine:3.4 FROM alpine:3.4
ARG version
RUN apk -U add \ RUN apk -U add \
python \ python \
py-pip py-pip
@ -7,7 +8,7 @@ RUN apk -U add \
COPY requirements.txt /code/requirements.txt COPY requirements.txt /code/requirements.txt
RUN pip install -r /code/requirements.txt RUN pip install -r /code/requirements.txt
ADD dist/docker-compose-release.tar.gz /code/docker-compose COPY dist/docker_compose-${version}-py2.py3-none-any.whl /code/
RUN pip install --no-deps /code/docker-compose/docker-compose-* RUN pip install --no-deps /code/docker_compose-${version}-py2.py3-none-any.whl
ENTRYPOINT ["/usr/bin/docker-compose"] ENTRYPOINT ["/usr/bin/docker-compose"]

View file

@ -35,7 +35,7 @@ A `docker-compose.yml` looks like this:
image: redis image: redis
For more information about the Compose file, see the For more information about the Compose file, see the
[Compose file reference](https://github.com/docker/docker.github.io/blob/master/compose/compose-file.md) [Compose file reference](https://github.com/docker/docker.github.io/blob/master/compose/compose-file/compose-versioning.md)
Compose has commands for managing the whole lifecycle of your application: Compose has commands for managing the whole lifecycle of your application:

View file

@ -1,4 +1,4 @@
from __future__ import absolute_import from __future__ import absolute_import
from __future__ import unicode_literals from __future__ import unicode_literals
__version__ = '1.10.0dev' __version__ = '1.12.0dev'

View file

@ -202,7 +202,7 @@ def convert_service_to_bundle(name, service_dict, image_digest):
return container_config return container_config
# See https://github.com/docker/swarmkit/blob//agent/exec/container/container.go#L95 # See https://github.com/docker/swarmkit/blob/agent/exec/container/container.go#L95
def set_command_and_args(config, entrypoint, command): def set_command_and_args(config, entrypoint, command):
if isinstance(entrypoint, six.string_types): if isinstance(entrypoint, six.string_types):
entrypoint = split_command(entrypoint) entrypoint = split_command(entrypoint)

View file

@ -0,0 +1,37 @@
from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
import subprocess
import sys
# Attempt to detect https://github.com/docker/compose/issues/4344
try:
# We don't try importing pip because it messes with package imports
# on some Linux distros (Ubuntu, Fedora)
# https://github.com/docker/compose/issues/4425
# https://github.com/docker/compose/issues/4481
# https://github.com/pypa/pip/blob/master/pip/_vendor/__init__.py
s_cmd = subprocess.Popen(
['pip', 'freeze'], stderr=subprocess.PIPE, stdout=subprocess.PIPE
)
packages = s_cmd.communicate()[0].splitlines()
dockerpy_installed = len(
list(filter(lambda p: p.startswith(b'docker-py=='), packages))
) > 0
if dockerpy_installed:
from .colors import red
print(
red('ERROR:'),
"Dependency conflict: an older version of the 'docker-py' package "
"is polluting the namespace. "
"Run the following command to remedy the issue:\n"
"pip uninstall docker docker-py; pip install docker",
file=sys.stderr
)
sys.exit(1)
except OSError:
# pip command is not available, which indicates it's probably the binary
# distribution of Compose which is not affected
pass

View file

@ -33,7 +33,7 @@ def make_color_fn(code):
return lambda s: ansi_color(code, s) return lambda s: ansi_color(code, s)
colorama.init() colorama.init(strip=False)
for (name, code) in get_pairs(): for (name, code) in get_pairs():
globals()[name] = make_color_fn(code) globals()[name] = make_color_fn(code)

View file

@ -191,6 +191,7 @@ class TopLevelCommand(object):
scale Set number of containers for a service scale Set number of containers for a service
start Start services start Start services
stop Stop services stop Stop services
top Display the running processes
unpause Unpause services unpause Unpause services
up Create and start containers up Create and start containers
version Show the Docker-Compose version information version Show the Docker-Compose version information
@ -776,6 +777,33 @@ class TopLevelCommand(object):
containers = self.project.restart(service_names=options['SERVICE'], timeout=timeout) containers = self.project.restart(service_names=options['SERVICE'], timeout=timeout)
exit_if(not containers, 'No containers to restart', 1) exit_if(not containers, 'No containers to restart', 1)
def top(self, options):
"""
Display the running processes
Usage: top [SERVICE...]
"""
containers = sorted(
self.project.containers(service_names=options['SERVICE'], stopped=False) +
self.project.containers(service_names=options['SERVICE'], one_off=OneOffFilter.only),
key=attrgetter('name')
)
for idx, container in enumerate(containers):
if idx > 0:
print()
top_data = self.project.client.top(container.name)
headers = top_data.get("Titles")
rows = []
for process in top_data.get("Processes", []):
rows.append(process)
print(container.name)
print(Formatter().table(headers, rows))
def unpause(self, options): def unpause(self, options):
""" """
Unpause services. Unpause services.

View file

@ -12,10 +12,12 @@ import six
import yaml import yaml
from cached_property import cached_property from cached_property import cached_property
from . import types
from ..const import COMPOSEFILE_V1 as V1 from ..const import COMPOSEFILE_V1 as V1
from ..const import COMPOSEFILE_V2_0 as V2_0 from ..const import COMPOSEFILE_V2_0 as V2_0
from ..const import COMPOSEFILE_V2_1 as V2_1 from ..const import COMPOSEFILE_V2_1 as V2_1
from ..const import COMPOSEFILE_V3_0 as V3_0 from ..const import COMPOSEFILE_V3_0 as V3_0
from ..const import COMPOSEFILE_V3_1 as V3_1
from ..utils import build_string_dict from ..utils import build_string_dict
from ..utils import parse_nanoseconds_int from ..utils import parse_nanoseconds_int
from ..utils import splitdrive from ..utils import splitdrive
@ -76,12 +78,13 @@ DOCKER_CONFIG_KEYS = [
'memswap_limit', 'memswap_limit',
'mem_swappiness', 'mem_swappiness',
'net', 'net',
'oom_score_adj' 'oom_score_adj',
'pid', 'pid',
'ports', 'ports',
'privileged', 'privileged',
'read_only', 'read_only',
'restart', 'restart',
'secrets',
'security_opt', 'security_opt',
'shm_size', 'shm_size',
'stdin_open', 'stdin_open',
@ -183,11 +186,6 @@ class ConfigFile(namedtuple('_ConfigFile', 'filename config')):
if version == '3': if version == '3':
version = V3_0 version = V3_0
if version not in (V2_0, V2_1, V3_0):
raise ConfigurationError(
'Version in "{}" is unsupported. {}'
.format(self.filename, VERSION_EXPLANATION))
return version return version
def get_service(self, name): def get_service(self, name):
@ -202,8 +200,11 @@ class ConfigFile(namedtuple('_ConfigFile', 'filename config')):
def get_networks(self): def get_networks(self):
return {} if self.version == V1 else self.config.get('networks', {}) return {} if self.version == V1 else self.config.get('networks', {})
def get_secrets(self):
return {} if self.version < V3_1 else self.config.get('secrets', {})
class Config(namedtuple('_Config', 'version services volumes networks')):
class Config(namedtuple('_Config', 'version services volumes networks secrets')):
""" """
:param version: configuration version :param version: configuration version
:type version: int :type version: int
@ -328,6 +329,7 @@ def load(config_details):
networks = load_mapping( networks = load_mapping(
config_details.config_files, 'get_networks', 'Network' config_details.config_files, 'get_networks', 'Network'
) )
secrets = load_secrets(config_details.config_files, config_details.working_dir)
service_dicts = load_services(config_details, main_file) service_dicts = load_services(config_details, main_file)
if main_file.version != V1: if main_file.version != V1:
@ -342,7 +344,7 @@ def load(config_details):
"`docker stack deploy` to deploy to a swarm." "`docker stack deploy` to deploy to a swarm."
.format(", ".join(sorted(s['name'] for s in services_using_deploy)))) .format(", ".join(sorted(s['name'] for s in services_using_deploy))))
return Config(main_file.version, service_dicts, volumes, networks) return Config(main_file.version, service_dicts, volumes, networks, secrets)
def load_mapping(config_files, get_func, entity_type): def load_mapping(config_files, get_func, entity_type):
@ -356,22 +358,12 @@ def load_mapping(config_files, get_func, entity_type):
external = config.get('external') external = config.get('external')
if external: if external:
if len(config.keys()) > 1: validate_external(entity_type, name, config)
raise ConfigurationError(
'{} {} declared as external but specifies'
' additional attributes ({}). '.format(
entity_type,
name,
', '.join([k for k in config.keys() if k != 'external'])
)
)
if isinstance(external, dict): if isinstance(external, dict):
config['external_name'] = external.get('name') config['external_name'] = external.get('name')
else: else:
config['external_name'] = name config['external_name'] = name
mapping[name] = config
if 'driver_opts' in config: if 'driver_opts' in config:
config['driver_opts'] = build_string_dict( config['driver_opts'] = build_string_dict(
config['driver_opts'] config['driver_opts']
@ -383,6 +375,39 @@ def load_mapping(config_files, get_func, entity_type):
return mapping return mapping
def validate_external(entity_type, name, config):
if len(config.keys()) <= 1:
return
raise ConfigurationError(
"{} {} declared as external but specifies additional attributes "
"({}).".format(
entity_type, name, ', '.join(k for k in config if k != 'external')))
def load_secrets(config_files, working_dir):
mapping = {}
for config_file in config_files:
for name, config in config_file.get_secrets().items():
mapping[name] = config or {}
if not config:
continue
external = config.get('external')
if external:
validate_external('Secret', name, config)
if isinstance(external, dict):
config['external_name'] = external.get('name')
else:
config['external_name'] = name
if 'file' in config:
config['file'] = expand_path(working_dir, config['file'])
return mapping
def load_services(config_details, config_file): def load_services(config_details, config_file):
def build_service(service_name, service_dict, service_names): def build_service(service_name, service_dict, service_names):
service_config = ServiceConfig.with_abs_paths( service_config = ServiceConfig.with_abs_paths(
@ -449,7 +474,7 @@ def process_config_file(config_file, environment, service_name=None):
'service', 'service',
environment) environment)
if config_file.version in (V2_0, V2_1, V3_0): if config_file.version in (V2_0, V2_1, V3_0, V3_1):
processed_config = dict(config_file.config) processed_config = dict(config_file.config)
processed_config['services'] = services processed_config['services'] = services
processed_config['volumes'] = interpolate_config_section( processed_config['volumes'] = interpolate_config_section(
@ -465,7 +490,9 @@ def process_config_file(config_file, environment, service_name=None):
elif config_file.version == V1: elif config_file.version == V1:
processed_config = services processed_config = services
else: else:
raise Exception("Unsupported version: {}".format(repr(config_file.version))) raise ConfigurationError(
'Version in "{}" is unsupported. {}'
.format(config_file.filename, VERSION_EXPLANATION))
config_file = config_file._replace(config=processed_config) config_file = config_file._replace(config=processed_config)
validate_against_config_schema(config_file) validate_against_config_schema(config_file)
@ -675,7 +702,7 @@ def process_healthcheck(service_dict, service_name):
hc = {} hc = {}
raw = service_dict['healthcheck'] raw = service_dict['healthcheck']
if raw.get('disable') or raw.get('disabled'): if raw.get('disable'):
if len(raw) > 1: if len(raw) > 1:
raise ConfigurationError( raise ConfigurationError(
'Service "{}" defines an invalid healthcheck: ' 'Service "{}" defines an invalid healthcheck: '
@ -686,9 +713,15 @@ def process_healthcheck(service_dict, service_name):
hc['test'] = raw['test'] hc['test'] = raw['test']
if 'interval' in raw: if 'interval' in raw:
hc['interval'] = parse_nanoseconds_int(raw['interval']) if not isinstance(raw['interval'], six.integer_types):
hc['interval'] = parse_nanoseconds_int(raw['interval'])
else: # Conversion has been done previously
hc['interval'] = raw['interval']
if 'timeout' in raw: if 'timeout' in raw:
hc['timeout'] = parse_nanoseconds_int(raw['timeout']) if not isinstance(raw['timeout'], six.integer_types):
hc['timeout'] = parse_nanoseconds_int(raw['timeout'])
else: # Conversion has been done previously
hc['timeout'] = raw['timeout']
if 'retries' in raw: if 'retries' in raw:
hc['retries'] = raw['retries'] hc['retries'] = raw['retries']
@ -712,7 +745,7 @@ def finalize_service(service_config, service_names, version, environment):
if 'volumes' in service_dict: if 'volumes' in service_dict:
service_dict['volumes'] = [ service_dict['volumes'] = [
VolumeSpec.parse( VolumeSpec.parse(
v, environment.get('COMPOSE_CONVERT_WINDOWS_PATHS') v, environment.get_boolean('COMPOSE_CONVERT_WINDOWS_PATHS')
) for v in service_dict['volumes'] ) for v in service_dict['volumes']
] ]
@ -730,6 +763,11 @@ def finalize_service(service_config, service_names, version, environment):
if 'restart' in service_dict: if 'restart' in service_dict:
service_dict['restart'] = parse_restart_spec(service_dict['restart']) service_dict['restart'] = parse_restart_spec(service_dict['restart'])
if 'secrets' in service_dict:
service_dict['secrets'] = [
types.ServiceSecret.parse(s) for s in service_dict['secrets']
]
normalize_build(service_dict, service_config.working_dir, environment) normalize_build(service_dict, service_config.working_dir, environment)
service_dict['name'] = service_config.name service_dict['name'] = service_config.name
@ -818,14 +856,16 @@ def merge_service_dicts(base, override, version):
md.merge_mapping('ulimits', parse_ulimits) md.merge_mapping('ulimits', parse_ulimits)
md.merge_mapping('networks', parse_networks) md.merge_mapping('networks', parse_networks)
md.merge_mapping('sysctls', parse_sysctls) md.merge_mapping('sysctls', parse_sysctls)
md.merge_mapping('depends_on', parse_depends_on)
md.merge_sequence('links', ServiceLink.parse) md.merge_sequence('links', ServiceLink.parse)
md.merge_sequence('secrets', types.ServiceSecret.parse)
for field in ['volumes', 'devices']: for field in ['volumes', 'devices']:
md.merge_field(field, merge_path_mappings) md.merge_field(field, merge_path_mappings)
for field in [ for field in [
'ports', 'cap_add', 'cap_drop', 'expose', 'external_links', 'ports', 'cap_add', 'cap_drop', 'expose', 'external_links',
'security_opt', 'volumes_from', 'depends_on', 'security_opt', 'volumes_from',
]: ]:
md.merge_field(field, merge_unique_items_lists, default=[]) md.merge_field(field, merge_unique_items_lists, default=[])
@ -920,6 +960,9 @@ parse_environment = functools.partial(parse_dict_or_list, split_env, 'environmen
parse_labels = functools.partial(parse_dict_or_list, split_kv, 'labels') parse_labels = functools.partial(parse_dict_or_list, split_kv, 'labels')
parse_networks = functools.partial(parse_dict_or_list, lambda k: (k, None), 'networks') parse_networks = functools.partial(parse_dict_or_list, lambda k: (k, None), 'networks')
parse_sysctls = functools.partial(parse_dict_or_list, split_kv, 'sysctls') parse_sysctls = functools.partial(parse_dict_or_list, split_kv, 'sysctls')
parse_depends_on = functools.partial(
parse_dict_or_list, lambda k: (k, {'condition': 'service_started'}), 'depends_on'
)
def parse_ulimits(ulimits): def parse_ulimits(ulimits):

View file

@ -192,6 +192,7 @@
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"shm_size": {"type": ["number", "string"]}, "shm_size": {"type": ["number", "string"]},
"stdin_open": {"type": "boolean"}, "stdin_open": {"type": "boolean"},
"stop_grace_period": {"type": "string", "format": "duration"},
"stop_signal": {"type": "string"}, "stop_signal": {"type": "string"},
"tmpfs": {"$ref": "#/definitions/string_or_list"}, "tmpfs": {"$ref": "#/definitions/string_or_list"},
"tty": {"type": "boolean"}, "tty": {"type": "boolean"},
@ -275,9 +276,9 @@
"type": ["boolean", "object"], "type": ["boolean", "object"],
"properties": { "properties": {
"name": {"type": "string"} "name": {"type": "string"}
} },
}, "additionalProperties": false
"additionalProperties": false }
}, },
"additionalProperties": false "additionalProperties": false
}, },

View file

@ -217,6 +217,7 @@
"shm_size": {"type": ["number", "string"]}, "shm_size": {"type": ["number", "string"]},
"sysctls": {"$ref": "#/definitions/list_or_dict"}, "sysctls": {"$ref": "#/definitions/list_or_dict"},
"stdin_open": {"type": "boolean"}, "stdin_open": {"type": "boolean"},
"stop_grace_period": {"type": "string", "format": "duration"},
"stop_signal": {"type": "string"}, "stop_signal": {"type": "string"},
"tmpfs": {"$ref": "#/definitions/string_or_list"}, "tmpfs": {"$ref": "#/definitions/string_or_list"},
"tty": {"type": "boolean"}, "tty": {"type": "boolean"},
@ -258,7 +259,7 @@
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"disabled": {"type": "boolean"}, "disable": {"type": "boolean"},
"interval": {"type": "string"}, "interval": {"type": "string"},
"retries": {"type": "number"}, "retries": {"type": "number"},
"test": { "test": {
@ -321,10 +322,10 @@
"type": ["boolean", "object"], "type": ["boolean", "object"],
"properties": { "properties": {
"name": {"type": "string"} "name": {"type": "string"}
} },
"additionalProperties": false
}, },
"labels": {"$ref": "#/definitions/list_or_dict"}, "labels": {"$ref": "#/definitions/list_or_dict"}
"additionalProperties": false
}, },
"additionalProperties": false "additionalProperties": false
}, },

View file

@ -169,8 +169,8 @@
"shm_size": {"type": ["number", "string"]}, "shm_size": {"type": ["number", "string"]},
"sysctls": {"$ref": "#/definitions/list_or_dict"}, "sysctls": {"$ref": "#/definitions/list_or_dict"},
"stdin_open": {"type": "boolean"}, "stdin_open": {"type": "boolean"},
"stop_signal": {"type": "string"},
"stop_grace_period": {"type": "string", "format": "duration"}, "stop_grace_period": {"type": "string", "format": "duration"},
"stop_signal": {"type": "string"},
"tmpfs": {"$ref": "#/definitions/string_or_list"}, "tmpfs": {"$ref": "#/definitions/string_or_list"},
"tty": {"type": "boolean"}, "tty": {"type": "boolean"},
"ulimits": { "ulimits": {
@ -202,10 +202,11 @@
"healthcheck": { "healthcheck": {
"id": "#/definitions/healthcheck", "id": "#/definitions/healthcheck",
"type": ["object", "null"], "type": "object",
"additionalProperties": false,
"properties": { "properties": {
"interval": {"type":"string"}, "disable": {"type": "boolean"},
"timeout": {"type":"string"}, "interval": {"type": "string"},
"retries": {"type": "number"}, "retries": {"type": "number"},
"test": { "test": {
"oneOf": [ "oneOf": [
@ -213,9 +214,8 @@
{"type": "array", "items": {"type": "string"}} {"type": "array", "items": {"type": "string"}}
] ]
}, },
"disable": {"type": "boolean"} "timeout": {"type": "string"}
}, }
"additionalProperties": false
}, },
"deployment": { "deployment": {
"id": "#/definitions/deployment", "id": "#/definitions/deployment",
@ -270,7 +270,7 @@
"cpus": {"type": "string"}, "cpus": {"type": "string"},
"memory": {"type": "string"} "memory": {"type": "string"}
}, },
"additionaProperties": false "additionalProperties": false
}, },
"network": { "network": {
@ -308,6 +308,7 @@
}, },
"additionalProperties": false "additionalProperties": false
}, },
"internal": {"type": "boolean"},
"labels": {"$ref": "#/definitions/list_or_dict"} "labels": {"$ref": "#/definitions/list_or_dict"}
}, },
"additionalProperties": false "additionalProperties": false
@ -328,10 +329,11 @@
"type": ["boolean", "object"], "type": ["boolean", "object"],
"properties": { "properties": {
"name": {"type": "string"} "name": {"type": "string"}
} },
} "additionalProperties": false
},
"labels": {"$ref": "#/definitions/list_or_dict"}
}, },
"labels": {"$ref": "#/definitions/list_or_dict"},
"additionalProperties": false "additionalProperties": false
}, },

View file

@ -0,0 +1,428 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "config_schema_v3.1.json",
"type": "object",
"required": ["version"],
"properties": {
"version": {
"type": "string"
},
"services": {
"id": "#/properties/services",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/service"
}
},
"additionalProperties": false
},
"networks": {
"id": "#/properties/networks",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/network"
}
}
},
"volumes": {
"id": "#/properties/volumes",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/volume"
}
},
"additionalProperties": false
},
"secrets": {
"id": "#/properties/secrets",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/secret"
}
},
"additionalProperties": false
}
},
"additionalProperties": false,
"definitions": {
"service": {
"id": "#/definitions/service",
"type": "object",
"properties": {
"deploy": {"$ref": "#/definitions/deployment"},
"build": {
"oneOf": [
{"type": "string"},
{
"type": "object",
"properties": {
"context": {"type": "string"},
"dockerfile": {"type": "string"},
"args": {"$ref": "#/definitions/list_or_dict"}
},
"additionalProperties": false
}
]
},
"cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"cgroup_parent": {"type": "string"},
"command": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
]
},
"container_name": {"type": "string"},
"depends_on": {"$ref": "#/definitions/list_of_strings"},
"devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"dns": {"$ref": "#/definitions/string_or_list"},
"dns_search": {"$ref": "#/definitions/string_or_list"},
"domainname": {"type": "string"},
"entrypoint": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
]
},
"env_file": {"$ref": "#/definitions/string_or_list"},
"environment": {"$ref": "#/definitions/list_or_dict"},
"expose": {
"type": "array",
"items": {
"type": ["string", "number"],
"format": "expose"
},
"uniqueItems": true
},
"external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"extra_hosts": {"$ref": "#/definitions/list_or_dict"},
"healthcheck": {"$ref": "#/definitions/healthcheck"},
"hostname": {"type": "string"},
"image": {"type": "string"},
"ipc": {"type": "string"},
"labels": {"$ref": "#/definitions/list_or_dict"},
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"logging": {
"type": "object",
"properties": {
"driver": {"type": "string"},
"options": {
"type": "object",
"patternProperties": {
"^.+$": {"type": ["string", "number", "null"]}
}
}
},
"additionalProperties": false
},
"mac_address": {"type": "string"},
"network_mode": {"type": "string"},
"networks": {
"oneOf": [
{"$ref": "#/definitions/list_of_strings"},
{
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"oneOf": [
{
"type": "object",
"properties": {
"aliases": {"$ref": "#/definitions/list_of_strings"},
"ipv4_address": {"type": "string"},
"ipv6_address": {"type": "string"}
},
"additionalProperties": false
},
{"type": "null"}
]
}
},
"additionalProperties": false
}
]
},
"pid": {"type": ["string", "null"]},
"ports": {
"type": "array",
"items": {
"type": ["string", "number"],
"format": "ports"
},
"uniqueItems": true
},
"privileged": {"type": "boolean"},
"read_only": {"type": "boolean"},
"restart": {"type": "string"},
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"shm_size": {"type": ["number", "string"]},
"secrets": {
"type": "array",
"items": {
"oneOf": [
{"type": "string"},
{
"type": "object",
"properties": {
"source": {"type": "string"},
"target": {"type": "string"},
"uid": {"type": "string"},
"gid": {"type": "string"},
"mode": {"type": "number"}
}
}
]
}
},
"sysctls": {"$ref": "#/definitions/list_or_dict"},
"stdin_open": {"type": "boolean"},
"stop_grace_period": {"type": "string", "format": "duration"},
"stop_signal": {"type": "string"},
"tmpfs": {"$ref": "#/definitions/string_or_list"},
"tty": {"type": "boolean"},
"ulimits": {
"type": "object",
"patternProperties": {
"^[a-z]+$": {
"oneOf": [
{"type": "integer"},
{
"type":"object",
"properties": {
"hard": {"type": "integer"},
"soft": {"type": "integer"}
},
"required": ["soft", "hard"],
"additionalProperties": false
}
]
}
}
},
"user": {"type": "string"},
"userns_mode": {"type": "string"},
"volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"working_dir": {"type": "string"}
},
"additionalProperties": false
},
"healthcheck": {
"id": "#/definitions/healthcheck",
"type": "object",
"additionalProperties": false,
"properties": {
"disable": {"type": "boolean"},
"interval": {"type": "string"},
"retries": {"type": "number"},
"test": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
]
},
"timeout": {"type": "string"}
}
},
"deployment": {
"id": "#/definitions/deployment",
"type": ["object", "null"],
"properties": {
"mode": {"type": "string"},
"replicas": {"type": "integer"},
"labels": {"$ref": "#/definitions/list_or_dict"},
"update_config": {
"type": "object",
"properties": {
"parallelism": {"type": "integer"},
"delay": {"type": "string", "format": "duration"},
"failure_action": {"type": "string"},
"monitor": {"type": "string", "format": "duration"},
"max_failure_ratio": {"type": "number"}
},
"additionalProperties": false
},
"resources": {
"type": "object",
"properties": {
"limits": {"$ref": "#/definitions/resource"},
"reservations": {"$ref": "#/definitions/resource"}
}
},
"restart_policy": {
"type": "object",
"properties": {
"condition": {"type": "string"},
"delay": {"type": "string", "format": "duration"},
"max_attempts": {"type": "integer"},
"window": {"type": "string", "format": "duration"}
},
"additionalProperties": false
},
"placement": {
"type": "object",
"properties": {
"constraints": {"type": "array", "items": {"type": "string"}}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"resource": {
"id": "#/definitions/resource",
"type": "object",
"properties": {
"cpus": {"type": "string"},
"memory": {"type": "string"}
},
"additionalProperties": false
},
"network": {
"id": "#/definitions/network",
"type": ["object", "null"],
"properties": {
"driver": {"type": "string"},
"driver_opts": {
"type": "object",
"patternProperties": {
"^.+$": {"type": ["string", "number"]}
}
},
"ipam": {
"type": "object",
"properties": {
"driver": {"type": "string"},
"config": {
"type": "array",
"items": {
"type": "object",
"properties": {
"subnet": {"type": "string"}
},
"additionalProperties": false
}
}
},
"additionalProperties": false
},
"external": {
"type": ["boolean", "object"],
"properties": {
"name": {"type": "string"}
},
"additionalProperties": false
},
"internal": {"type": "boolean"},
"labels": {"$ref": "#/definitions/list_or_dict"}
},
"additionalProperties": false
},
"volume": {
"id": "#/definitions/volume",
"type": ["object", "null"],
"properties": {
"driver": {"type": "string"},
"driver_opts": {
"type": "object",
"patternProperties": {
"^.+$": {"type": ["string", "number"]}
}
},
"external": {
"type": ["boolean", "object"],
"properties": {
"name": {"type": "string"}
},
"additionalProperties": false
},
"labels": {"$ref": "#/definitions/list_or_dict"}
},
"additionalProperties": false
},
"secret": {
"id": "#/definitions/secret",
"type": "object",
"properties": {
"file": {"type": "string"},
"external": {
"type": ["boolean", "object"],
"properties": {
"name": {"type": "string"}
}
},
"labels": {"$ref": "#/definitions/list_or_dict"}
},
"additionalProperties": false
},
"string_or_list": {
"oneOf": [
{"type": "string"},
{"$ref": "#/definitions/list_of_strings"}
]
},
"list_of_strings": {
"type": "array",
"items": {"type": "string"},
"uniqueItems": true
},
"list_or_dict": {
"oneOf": [
{
"type": "object",
"patternProperties": {
".+": {
"type": ["string", "number", "null"]
}
},
"additionalProperties": false
},
{"type": "array", "items": {"type": "string"}, "uniqueItems": true}
]
},
"constraints": {
"service": {
"id": "#/definitions/constraints/service",
"anyOf": [
{"required": ["build"]},
{"required": ["image"]}
],
"properties": {
"build": {
"required": ["context"]
}
}
}
}
}
}

View file

@ -2,6 +2,7 @@ from __future__ import absolute_import
from __future__ import unicode_literals from __future__ import unicode_literals
import codecs import codecs
import contextlib
import logging import logging
import os import os
@ -31,11 +32,12 @@ def env_vars_from_file(filename):
elif not os.path.isfile(filename): elif not os.path.isfile(filename):
raise ConfigurationError("%s is not a file." % (filename)) raise ConfigurationError("%s is not a file." % (filename))
env = {} env = {}
for line in codecs.open(filename, 'r', 'utf-8'): with contextlib.closing(codecs.open(filename, 'r', 'utf-8')) as fileobj:
line = line.strip() for line in fileobj:
if line and not line.startswith('#'): line = line.strip()
k, v = split_env(line) if line and not line.startswith('#'):
env[k] = v k, v = split_env(line)
env[k] = v
return env return env
@ -105,3 +107,14 @@ class Environment(dict):
super(Environment, self).get(key.upper(), *args, **kwargs) super(Environment, self).get(key.upper(), *args, **kwargs)
) )
return super(Environment, self).get(key, *args, **kwargs) return super(Environment, self).get(key, *args, **kwargs)
def get_boolean(self, key):
# Convert a value to a boolean using "common sense" rules.
# Unset, empty, "0" and "false" (i-case) yield False.
# All other values yield True.
value = self.get(key)
if not value:
return False
if value.lower() in ['0', 'false']:
return False
return True

View file

@ -32,6 +32,11 @@ def denormalize_config(config):
if 'external_name' in net_conf: if 'external_name' in net_conf:
del net_conf['external_name'] del net_conf['external_name']
volumes = config.volumes.copy()
for vol_name, vol_conf in volumes.items():
if 'external_name' in vol_conf:
del vol_conf['external_name']
version = config.version version = config.version
if version == V1: if version == V1:
version = V2_1 version = V2_1
@ -40,7 +45,7 @@ def denormalize_config(config):
'version': version, 'version': version,
'services': services, 'services': services,
'networks': networks, 'networks': networks,
'volumes': config.volumes, 'volumes': volumes,
} }
@ -52,13 +57,52 @@ def serialize_config(config):
width=80) width=80)
def serialize_ns_time_value(value):
result = (value, 'ns')
table = [
(1000., 'us'),
(1000., 'ms'),
(1000., 's'),
(60., 'm'),
(60., 'h')
]
for stage in table:
tmp = value / stage[0]
if tmp == int(value / stage[0]):
value = tmp
result = (int(value), stage[1])
else:
break
return '{0}{1}'.format(*result)
def denormalize_service_dict(service_dict, version): def denormalize_service_dict(service_dict, version):
service_dict = service_dict.copy() service_dict = service_dict.copy()
if 'restart' in service_dict: if 'restart' in service_dict:
service_dict['restart'] = types.serialize_restart_spec(service_dict['restart']) service_dict['restart'] = types.serialize_restart_spec(
service_dict['restart']
)
if version == V1 and 'network_mode' not in service_dict: if version == V1 and 'network_mode' not in service_dict:
service_dict['network_mode'] = 'bridge' service_dict['network_mode'] = 'bridge'
if 'depends_on' in service_dict and version != V2_1:
service_dict['depends_on'] = sorted([
svc for svc in service_dict['depends_on'].keys()
])
if 'healthcheck' in service_dict:
if 'interval' in service_dict['healthcheck']:
service_dict['healthcheck']['interval'] = serialize_ns_time_value(
service_dict['healthcheck']['interval']
)
if 'timeout' in service_dict['healthcheck']:
service_dict['healthcheck']['timeout'] = serialize_ns_time_value(
service_dict['healthcheck']['timeout']
)
if 'secrets' in service_dict:
service_dict['secrets'] = map(lambda s: s.repr(), service_dict['secrets'])
return service_dict return service_dict

View file

@ -10,8 +10,8 @@ from collections import namedtuple
import six import six
from compose.config.config import V1 from ..const import COMPOSEFILE_V1 as V1
from compose.config.errors import ConfigurationError from .errors import ConfigurationError
from compose.const import IS_WINDOWS_PLATFORM from compose.const import IS_WINDOWS_PLATFORM
from compose.utils import splitdrive from compose.utils import splitdrive
@ -234,3 +234,27 @@ class ServiceLink(namedtuple('_ServiceLink', 'target alias')):
@property @property
def merge_field(self): def merge_field(self):
return self.alias return self.alias
class ServiceSecret(namedtuple('_ServiceSecret', 'source target uid gid mode')):
@classmethod
def parse(cls, spec):
if isinstance(spec, six.string_types):
return cls(spec, None, None, None, None)
return cls(
spec.get('source'),
spec.get('target'),
spec.get('uid'),
spec.get('gid'),
spec.get('mode'),
)
@property
def merge_field(self):
return self.source
def repr(self):
return dict(
[(k, v) for k, v in self._asdict().items() if v is not None]
)

View file

@ -5,7 +5,7 @@ import sys
DEFAULT_TIMEOUT = 10 DEFAULT_TIMEOUT = 10
HTTP_TIMEOUT = 60 HTTP_TIMEOUT = 60
IMAGE_EVENTS = ['delete', 'import', 'pull', 'push', 'tag', 'untag'] IMAGE_EVENTS = ['delete', 'import', 'load', 'pull', 'push', 'save', 'tag', 'untag']
IS_WINDOWS_PLATFORM = (sys.platform == "win32") IS_WINDOWS_PLATFORM = (sys.platform == "win32")
LABEL_CONTAINER_NUMBER = 'com.docker.compose.container-number' LABEL_CONTAINER_NUMBER = 'com.docker.compose.container-number'
LABEL_ONE_OFF = 'com.docker.compose.oneoff' LABEL_ONE_OFF = 'com.docker.compose.oneoff'
@ -16,16 +16,20 @@ LABEL_VERSION = 'com.docker.compose.version'
LABEL_VOLUME = 'com.docker.compose.volume' LABEL_VOLUME = 'com.docker.compose.volume'
LABEL_CONFIG_HASH = 'com.docker.compose.config-hash' LABEL_CONFIG_HASH = 'com.docker.compose.config-hash'
SECRETS_PATH = '/run/secrets'
COMPOSEFILE_V1 = '1' COMPOSEFILE_V1 = '1'
COMPOSEFILE_V2_0 = '2.0' COMPOSEFILE_V2_0 = '2.0'
COMPOSEFILE_V2_1 = '2.1' COMPOSEFILE_V2_1 = '2.1'
COMPOSEFILE_V3_0 = '3.0' COMPOSEFILE_V3_0 = '3.0'
COMPOSEFILE_V3_1 = '3.1'
API_VERSIONS = { API_VERSIONS = {
COMPOSEFILE_V1: '1.21', COMPOSEFILE_V1: '1.21',
COMPOSEFILE_V2_0: '1.22', COMPOSEFILE_V2_0: '1.22',
COMPOSEFILE_V2_1: '1.24', COMPOSEFILE_V2_1: '1.24',
COMPOSEFILE_V3_0: '1.25', COMPOSEFILE_V3_0: '1.25',
COMPOSEFILE_V3_1: '1.25',
} }
API_VERSION_TO_ENGINE_VERSION = { API_VERSION_TO_ENGINE_VERSION = {
@ -33,4 +37,5 @@ API_VERSION_TO_ENGINE_VERSION = {
API_VERSIONS[COMPOSEFILE_V2_0]: '1.10.0', API_VERSIONS[COMPOSEFILE_V2_0]: '1.10.0',
API_VERSIONS[COMPOSEFILE_V2_1]: '1.12.0', API_VERSIONS[COMPOSEFILE_V2_1]: '1.12.0',
API_VERSIONS[COMPOSEFILE_V3_0]: '1.13.0', API_VERSIONS[COMPOSEFILE_V3_0]: '1.13.0',
API_VERSIONS[COMPOSEFILE_V3_1]: '1.13.0',
} }

View file

@ -12,6 +12,8 @@ from six.moves.queue import Empty
from six.moves.queue import Queue from six.moves.queue import Queue
from compose.cli.signals import ShutdownException from compose.cli.signals import ShutdownException
from compose.errors import HealthCheckFailed
from compose.errors import NoHealthCheckConfigured
from compose.errors import OperationFailedError from compose.errors import OperationFailedError
from compose.utils import get_output_stream from compose.utils import get_output_stream
@ -48,7 +50,7 @@ def parallel_execute(objects, func, get_name, msg, get_deps=None):
elif isinstance(exception, APIError): elif isinstance(exception, APIError):
errors[get_name(obj)] = exception.explanation errors[get_name(obj)] = exception.explanation
writer.write(get_name(obj), 'error') writer.write(get_name(obj), 'error')
elif isinstance(exception, OperationFailedError): elif isinstance(exception, (OperationFailedError, HealthCheckFailed, NoHealthCheckConfigured)):
errors[get_name(obj)] = exception.msg errors[get_name(obj)] = exception.msg
writer.write(get_name(obj), 'error') writer.write(get_name(obj), 'error')
elif isinstance(exception, UpstreamError): elif isinstance(exception, UpstreamError):
@ -164,21 +166,27 @@ def feed_queue(objects, func, get_deps, results, state):
for obj in pending: for obj in pending:
deps = get_deps(obj) deps = get_deps(obj)
try:
if any(dep[0] in state.failed for dep in deps): if any(dep[0] in state.failed for dep in deps):
log.debug('{} has upstream errors - not processing'.format(obj)) log.debug('{} has upstream errors - not processing'.format(obj))
results.put((obj, None, UpstreamError())) results.put((obj, None, UpstreamError()))
state.failed.add(obj) state.failed.add(obj)
elif all( elif all(
dep not in objects or ( dep not in objects or (
dep in state.finished and (not ready_check or ready_check(dep)) dep in state.finished and (not ready_check or ready_check(dep))
) for dep, ready_check in deps ) for dep, ready_check in deps
): ):
log.debug('Starting producer thread for {}'.format(obj)) log.debug('Starting producer thread for {}'.format(obj))
t = Thread(target=producer, args=(obj, func, results)) t = Thread(target=producer, args=(obj, func, results))
t.daemon = True t.daemon = True
t.start() t.start()
state.started.add(obj) state.started.add(obj)
except (HealthCheckFailed, NoHealthCheckConfigured) as e:
log.debug(
'Healthcheck for service(s) upstream of {} failed - '
'not processing'.format(obj)
)
results.put((obj, None, e))
if state.is_done(): if state.is_done():
results.put(STOP) results.put(STOP)

View file

@ -104,6 +104,11 @@ class Project(object):
for volume_spec in service_dict.get('volumes', []) for volume_spec in service_dict.get('volumes', [])
] ]
secrets = get_secrets(
service_dict['name'],
service_dict.pop('secrets', None) or [],
config_data.secrets)
project.services.append( project.services.append(
Service( Service(
service_dict.pop('name'), service_dict.pop('name'),
@ -114,6 +119,7 @@ class Project(object):
links=links, links=links,
network_mode=network_mode, network_mode=network_mode,
volumes_from=volumes_from, volumes_from=volumes_from,
secrets=secrets,
**service_dict) **service_dict)
) )
@ -359,7 +365,7 @@ class Project(object):
# TODO: get labels from the API v1.22 , see github issue 2618 # TODO: get labels from the API v1.22 , see github issue 2618
try: try:
# this can fail if the conatiner has been removed # this can fail if the container has been removed
container = Container.from_id(self.client, event['id']) container = Container.from_id(self.client, event['id'])
except APIError: except APIError:
continue continue
@ -553,6 +559,33 @@ def get_volumes_from(project, service_dict):
return [build_volume_from(vf) for vf in volumes_from] return [build_volume_from(vf) for vf in volumes_from]
def get_secrets(service, service_secrets, secret_defs):
secrets = []
for secret in service_secrets:
secret_def = secret_defs.get(secret.source)
if not secret_def:
raise ConfigurationError(
"Service \"{service}\" uses an undefined secret \"{secret}\" "
.format(service=service, secret=secret.source))
if secret_def.get('external_name'):
log.warn("Service \"{service}\" uses secret \"{secret}\" which is external. "
"External secrets are not available to containers created by "
"docker-compose.".format(service=service, secret=secret.source))
continue
if secret.uid or secret.gid or secret.mode:
log.warn("Service \"{service}\" uses secret \"{secret}\" with uid, "
"gid, or mode. These fields are not supported by this "
"implementation of the Compose file".format(
service=service, secret=secret.source))
secrets.append({'secret': secret, 'file': secret_def.get('file')})
return secrets
def warn_for_swarm_mode(client): def warn_for_swarm_mode(client):
info = client.info() info = client.info()
if info.get('Swarm', {}).get('LocalNodeState') == 'active': if info.get('Swarm', {}).get('LocalNodeState') == 'active':

View file

@ -10,17 +10,20 @@ from operator import attrgetter
import enum import enum
import six import six
from docker.errors import APIError from docker.errors import APIError
from docker.errors import ImageNotFound
from docker.errors import NotFound from docker.errors import NotFound
from docker.types import LogConfig from docker.types import LogConfig
from docker.utils.ports import build_port_bindings from docker.utils.ports import build_port_bindings
from docker.utils.ports import split_port from docker.utils.ports import split_port
from . import __version__ from . import __version__
from . import const
from . import progress_stream from . import progress_stream
from .config import DOCKER_CONFIG_KEYS from .config import DOCKER_CONFIG_KEYS
from .config import merge_environment from .config import merge_environment
from .config.types import VolumeSpec from .config.types import VolumeSpec
from .const import DEFAULT_TIMEOUT from .const import DEFAULT_TIMEOUT
from .const import IS_WINDOWS_PLATFORM
from .const import LABEL_CONFIG_HASH from .const import LABEL_CONFIG_HASH
from .const import LABEL_CONTAINER_NUMBER from .const import LABEL_CONTAINER_NUMBER
from .const import LABEL_ONE_OFF from .const import LABEL_ONE_OFF
@ -137,6 +140,7 @@ class Service(object):
volumes_from=None, volumes_from=None,
network_mode=None, network_mode=None,
networks=None, networks=None,
secrets=None,
**options **options
): ):
self.name = name self.name = name
@ -147,6 +151,7 @@ class Service(object):
self.volumes_from = volumes_from or [] self.volumes_from = volumes_from or []
self.network_mode = network_mode or NetworkMode(None) self.network_mode = network_mode or NetworkMode(None)
self.networks = networks or {} self.networks = networks or {}
self.secrets = secrets or []
self.options = options self.options = options
def __repr__(self): def __repr__(self):
@ -323,11 +328,8 @@ class Service(object):
def image(self): def image(self):
try: try:
return self.client.inspect_image(self.image_name) return self.client.inspect_image(self.image_name)
except APIError as e: except ImageNotFound:
if e.response.status_code == 404 and e.explanation and 'No such image' in str(e.explanation): raise NoSuchImageError("Image '{}' not found".format(self.image_name))
raise NoSuchImageError("Image '{}' not found".format(self.image_name))
else:
raise
@property @property
def image_name(self): def image_name(self):
@ -693,9 +695,14 @@ class Service(object):
override_options['binds'] = binds override_options['binds'] = binds
container_options['environment'].update(affinity) container_options['environment'].update(affinity)
if 'volumes' in container_options: container_options['volumes'] = dict(
container_options['volumes'] = dict( (v.internal, {}) for v in container_options.get('volumes') or {})
(v.internal, {}) for v in container_options['volumes'])
secret_volumes = self.get_secret_volumes()
if secret_volumes:
override_options['binds'].extend(v.repr() for v in secret_volumes)
container_options['volumes'].update(
(v.internal, {}) for v in secret_volumes)
container_options['image'] = self.image_name container_options['image'] = self.image_name
@ -766,14 +773,23 @@ class Service(object):
return host_config return host_config
def get_secret_volumes(self):
def build_spec(secret):
target = '{}/{}'.format(
const.SECRETS_PATH,
secret['secret'].target or secret['secret'].source)
return VolumeSpec(secret['file'], target, 'ro')
return [build_spec(secret) for secret in self.secrets]
def build(self, no_cache=False, pull=False, force_rm=False): def build(self, no_cache=False, pull=False, force_rm=False):
log.info('Building %s' % self.name) log.info('Building %s' % self.name)
build_opts = self.options.get('build', {}) build_opts = self.options.get('build', {})
path = build_opts.get('context') path = build_opts.get('context')
# python2 os.path() doesn't support unicode, so we need to encode it to # python2 os.stat() doesn't support unicode on some UNIX, so we
# a byte string # encode it to a bytestring to be safe
if not six.PY3: if not six.PY3 and not IS_WINDOWS_PLATFORM:
path = path.encode('utf8') path = path.encode('utf8')
build_output = self.client.build( build_output = self.client.build(

View file

@ -434,6 +434,18 @@ _docker_compose_stop() {
} }
_docker_compose_top() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--help" -- "$cur" ) )
;;
*)
__docker_compose_services_running
;;
esac
}
_docker_compose_unpause() { _docker_compose_unpause() {
case "$cur" in case "$cur" in
-*) -*)
@ -499,6 +511,7 @@ _docker_compose() {
scale scale
start start
stop stop
top
unpause unpause
up up
version version

View file

@ -341,6 +341,11 @@ __docker-compose_subcommand() {
$opts_timeout \ $opts_timeout \
'*:running services:__docker-compose_runningservices' && ret=0 '*:running services:__docker-compose_runningservices' && ret=0
;; ;;
(top)
_arguments \
$opts_help \
'*:running services:__docker-compose_runningservices' && ret=0
;;
(unpause) (unpause)
_arguments \ _arguments \
$opts_help \ $opts_help \
@ -386,9 +391,17 @@ _docker-compose() {
integer ret=1 integer ret=1
typeset -A opt_args typeset -A opt_args
local file_description
if [[ -n ${words[(r)-f]} || -n ${words[(r)--file]} ]] ; then
file_description="Specify an override docker-compose file (default: docker-compose.override.yml)"
else
file_description="Specify an alternate docker-compose file (default: docker-compose.yml)"
fi
_arguments -C \ _arguments -C \
'(- :)'{-h,--help}'[Get help]' \ '(- :)'{-h,--help}'[Get help]' \
'(-f --file)'{-f,--file}'[Specify an alternate docker-compose file (default: docker-compose.yml)]:file:_files -g "*.yml"' \ '*'{-f,--file}"[${file_description}]:file:_files -g '*.yml'" \
'(-p --project-name)'{-p,--project-name}'[Specify an alternate project name (default: directory name)]:project name:' \ '(-p --project-name)'{-p,--project-name}'[Specify an alternate project name (default: directory name)]:project name:' \
'--verbose[Show more output]' \ '--verbose[Show more output]' \
'(- :)'{-v,--version}'[Print version and exit]' \ '(- :)'{-v,--version}'[Print version and exit]' \

View file

@ -37,6 +37,11 @@ exe = EXE(pyz,
'compose/config/config_schema_v3.0.json', 'compose/config/config_schema_v3.0.json',
'DATA' 'DATA'
), ),
(
'compose/config/config_schema_v3.1.json',
'compose/config/config_schema_v3.1.json',
'DATA'
),
( (
'compose/GITSHA', 'compose/GITSHA',
'compose/GITSHA', 'compose/GITSHA',

View file

@ -1 +1 @@
pyinstaller==3.1.1 pyinstaller==3.2.1

View file

@ -2,7 +2,7 @@ PyYAML==3.11
backports.ssl-match-hostname==3.5.0.1; python_version < '3' backports.ssl-match-hostname==3.5.0.1; python_version < '3'
cached-property==1.2.0 cached-property==1.2.0
colorama==0.3.7 colorama==0.3.7
docker==2.0.0 docker==2.0.2
dockerpty==0.4.1 dockerpty==0.4.1
docopt==0.6.1 docopt==0.6.1
enum34==1.0.4; python_version < '3.4' enum34==1.0.4; python_version < '3.4'

View file

@ -11,6 +11,5 @@ TAG=$1
VERSION="$(python setup.py --version)" VERSION="$(python setup.py --version)"
./script/build/write-git-sha ./script/build/write-git-sha
python setup.py sdist python setup.py sdist bdist_wheel
cp dist/docker-compose-$VERSION.tar.gz dist/docker-compose-release.tar.gz docker build --build-arg version=$VERSION -t docker/compose:$TAG -f Dockerfile.run .
docker build -t docker/compose:$TAG -f Dockerfile.run .

View file

@ -54,18 +54,19 @@ git push $GITHUB_REPO $VERSION
echo "Uploading the docker image" echo "Uploading the docker image"
docker push docker/compose:$VERSION docker push docker/compose:$VERSION
echo "Uploading sdist to PyPI" echo "Uploading package to PyPI"
pandoc -f markdown -t rst README.md -o README.rst pandoc -f markdown -t rst README.md -o README.rst
sed -i -e 's/logo.png?raw=true/https:\/\/github.com\/docker\/compose\/raw\/master\/logo.png?raw=true/' README.rst sed -i -e 's/logo.png?raw=true/https:\/\/github.com\/docker\/compose\/raw\/master\/logo.png?raw=true/' README.rst
./script/build/write-git-sha ./script/build/write-git-sha
python setup.py sdist python setup.py sdist bdist_wheel
if [ "$(command -v twine 2> /dev/null)" ]; then if [ "$(command -v twine 2> /dev/null)" ]; then
twine upload ./dist/docker-compose-${VERSION/-/}.tar.gz twine upload ./dist/docker-compose-${VERSION/-/}.tar.gz ./dist/docker_compose-${VERSION/-/}-py2.py3-none-any.whl
else else
python setup.py upload python setup.py upload
fi fi
echo "Testing pip package" echo "Testing pip package"
deactivate || true
virtualenv venv-test virtualenv venv-test
source venv-test/bin/activate source venv-test/bin/activate
pip install docker-compose==$VERSION pip install docker-compose==$VERSION

View file

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# #
# Util functions for release scritps # Util functions for release scripts
# #
set -e set -e

View file

@ -15,7 +15,7 @@
set -e set -e
VERSION="1.8.0" VERSION="1.12.0dev"
IMAGE="docker/compose:$VERSION" IMAGE="docker/compose:$VERSION"

View file

@ -5,7 +5,7 @@ version tags for recent releases, or the default release.
The default release is the most recent non-RC version. The default release is the most recent non-RC version.
Recent is a list of unqiue major.minor versions, where each is the most Recent is a list of unique major.minor versions, where each is the most
recent version in the series. recent version in the series.
For example, if the list of versions is: For example, if the list of versions is:

2
setup.cfg Normal file
View file

@ -0,0 +1,2 @@
[bdist_wheel]
universal=1

View file

@ -1,6 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import absolute_import from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals from __future__ import unicode_literals
import codecs import codecs
@ -8,6 +9,7 @@ import os
import re import re
import sys import sys
import pkg_resources
from setuptools import find_packages from setuptools import find_packages
from setuptools import setup from setuptools import setup
@ -35,7 +37,7 @@ install_requires = [
'requests >= 2.6.1, != 2.11.0, < 2.12', 'requests >= 2.6.1, != 2.11.0, < 2.12',
'texttable >= 0.8.1, < 0.9', 'texttable >= 0.8.1, < 0.9',
'websocket-client >= 0.32.0, < 1.0', 'websocket-client >= 0.32.0, < 1.0',
'docker >= 2.0.0, < 3.0', 'docker >= 2.0.2, < 3.0',
'dockerpty >= 0.4.1, < 0.5', 'dockerpty >= 0.4.1, < 0.5',
'six >= 1.3.0, < 2', 'six >= 1.3.0, < 2',
'jsonschema >= 2.5.1, < 3', 'jsonschema >= 2.5.1, < 3',
@ -49,7 +51,25 @@ tests_require = [
if sys.version_info[:2] < (3, 4): if sys.version_info[:2] < (3, 4):
tests_require.append('mock >= 1.0.1') tests_require.append('mock >= 1.0.1')
install_requires.append('enum34 >= 1.0.4, < 2')
extras_require = {
':python_version < "3.4"': ['enum34 >= 1.0.4, < 2'],
':python_version < "3.5"': ['backports.ssl_match_hostname >= 3.5'],
':python_version < "3.3"': ['ipaddress >= 1.0.16'],
}
try:
if 'bdist_wheel' not in sys.argv:
for key, value in extras_require.items():
if key.startswith(':') and pkg_resources.evaluate_marker(key[1:]):
install_requires.extend(value)
except Exception as e:
print("Failed to compute platform dependencies: {}. ".format(e) +
"All dependencies will be installed as a result.", file=sys.stderr)
for key, value in extras_require.items():
if key.startswith(':'):
install_requires.extend(value)
setup( setup(
@ -63,6 +83,7 @@ setup(
include_package_data=True, include_package_data=True,
test_suite='nose.collector', test_suite='nose.collector',
install_requires=install_requires, install_requires=install_requires,
extras_require=extras_require,
tests_require=tests_require, tests_require=tests_require,
entry_points=""" entry_points="""
[console_scripts] [console_scripts]

View file

@ -262,6 +262,20 @@ class CLITestCase(DockerClientTestCase):
} }
} }
def test_config_external_volume(self):
self.base_dir = 'tests/fixtures/volumes'
result = self.dispatch(['-f', 'external-volumes.yml', 'config'])
json_result = yaml.load(result.stdout)
assert 'volumes' in json_result
assert json_result['volumes'] == {
'foo': {
'external': True
},
'bar': {
'external': {'name': 'some_bar'}
}
}
def test_config_v1(self): def test_config_v1(self):
self.base_dir = 'tests/fixtures/v1-config' self.base_dir = 'tests/fixtures/v1-config'
result = self.dispatch(['config']) result = self.dispatch(['config'])
@ -295,7 +309,13 @@ class CLITestCase(DockerClientTestCase):
assert yaml.load(result.stdout) == { assert yaml.load(result.stdout) == {
'version': '3.0', 'version': '3.0',
'networks': {}, 'networks': {},
'volumes': {}, 'volumes': {
'foobar': {
'labels': {
'com.docker.compose.test': 'true',
},
},
},
'services': { 'services': {
'web': { 'web': {
'image': 'busybox', 'image': 'busybox',
@ -333,8 +353,8 @@ class CLITestCase(DockerClientTestCase):
'healthcheck': { 'healthcheck': {
'test': 'cat /etc/passwd', 'test': 'cat /etc/passwd',
'interval': 10000000000, 'interval': '10s',
'timeout': 1000000000, 'timeout': '1s',
'retries': 5, 'retries': 5,
}, },
@ -1214,7 +1234,7 @@ class CLITestCase(DockerClientTestCase):
container = service.containers(stopped=True, one_off=OneOffFilter.only)[0] container = service.containers(stopped=True, one_off=OneOffFilter.only)[0]
self.assertEqual(user, container.get('Config.User')) self.assertEqual(user, container.get('Config.User'))
def test_run_service_with_environement_overridden(self): def test_run_service_with_environment_overridden(self):
name = 'service' name = 'service'
self.base_dir = 'tests/fixtures/environment-composefile' self.base_dir = 'tests/fixtures/environment-composefile'
self.dispatch([ self.dispatch([
@ -1226,9 +1246,9 @@ class CLITestCase(DockerClientTestCase):
]) ])
service = self.project.get_service(name) service = self.project.get_service(name)
container = service.containers(stopped=True, one_off=OneOffFilter.only)[0] container = service.containers(stopped=True, one_off=OneOffFilter.only)[0]
# env overriden # env overridden
self.assertEqual('notbar', container.environment['foo']) self.assertEqual('notbar', container.environment['foo'])
# keep environement from yaml # keep environment from yaml
self.assertEqual('world', container.environment['hello']) self.assertEqual('world', container.environment['hello'])
# added option from command line # added option from command line
self.assertEqual('beta', container.environment['alpha']) self.assertEqual('beta', container.environment['alpha'])
@ -1273,7 +1293,7 @@ class CLITestCase(DockerClientTestCase):
self.assertEqual(port_range[0], "0.0.0.0:49153") self.assertEqual(port_range[0], "0.0.0.0:49153")
self.assertEqual(port_range[1], "0.0.0.0:49154") self.assertEqual(port_range[1], "0.0.0.0:49154")
def test_run_service_with_explicitly_maped_ports(self): def test_run_service_with_explicitly_mapped_ports(self):
# create one off container # create one off container
self.base_dir = 'tests/fixtures/ports-composefile' self.base_dir = 'tests/fixtures/ports-composefile'
self.dispatch(['run', '-d', '-p', '30000:3000', '--publish', '30001:3001', 'simple']) self.dispatch(['run', '-d', '-p', '30000:3000', '--publish', '30001:3001', 'simple'])
@ -1290,7 +1310,7 @@ class CLITestCase(DockerClientTestCase):
self.assertEqual(port_short, "0.0.0.0:30000") self.assertEqual(port_short, "0.0.0.0:30000")
self.assertEqual(port_full, "0.0.0.0:30001") self.assertEqual(port_full, "0.0.0.0:30001")
def test_run_service_with_explicitly_maped_ip_ports(self): def test_run_service_with_explicitly_mapped_ip_ports(self):
# create one off container # create one off container
self.base_dir = 'tests/fixtures/ports-composefile' self.base_dir = 'tests/fixtures/ports-composefile'
self.dispatch([ self.dispatch([
@ -1887,3 +1907,23 @@ class CLITestCase(DockerClientTestCase):
"BAZ=2", "BAZ=2",
]) ])
self.assertTrue(expected_env <= set(web.get('Config.Env'))) self.assertTrue(expected_env <= set(web.get('Config.Env')))
def test_top_services_not_running(self):
self.base_dir = 'tests/fixtures/top'
result = self.dispatch(['top'])
assert len(result.stdout) == 0
def test_top_services_running(self):
self.base_dir = 'tests/fixtures/top'
self.dispatch(['up', '-d'])
result = self.dispatch(['top'])
self.assertIn('top_service_a', result.stdout)
self.assertIn('top_service_b', result.stdout)
self.assertNotIn('top_not_a_service', result.stdout)
def test_top_processes_running(self):
self.base_dir = 'tests/fixtures/top'
self.dispatch(['up', '-d'])
result = self.dispatch(['top'])
assert result.stdout.count("top") == 4

View file

@ -0,0 +1,9 @@
version: '2.1'
services:
demo:
image: foobar:latest
healthcheck:
test: ["CMD", "/health.sh"]
interval: 10s
timeout: 5s
retries: 36

View file

@ -0,0 +1,6 @@
version: '2.1'
services:
demo:
extends:
file: healthcheck-1.yml
service: demo

1
tests/fixtures/secrets/default vendored Normal file
View file

@ -0,0 +1 @@
This is the secret

6
tests/fixtures/top/docker-compose.yml vendored Normal file
View file

@ -0,0 +1,6 @@
service_a:
image: busybox:latest
command: top
service_b:
image: busybox:latest
command: top

View file

@ -35,3 +35,7 @@ services:
retries: 5 retries: 5
stop_grace_period: 20s stop_grace_period: 20s
volumes:
foobar:
labels:
com.docker.compose.test: 'true'

View file

@ -0,0 +1,2 @@
version: '2.1'
services: {}

View file

@ -0,0 +1,16 @@
version: "2.1"
services:
web:
image: busybox
command: top
volumes:
- foo:/var/lib/
- bar:/etc/
volumes:
foo:
external: true
bar:
external:
name: some_bar

View file

@ -1,6 +1,7 @@
from __future__ import absolute_import from __future__ import absolute_import
from __future__ import unicode_literals from __future__ import unicode_literals
import os.path
import random import random
import py import py
@ -8,12 +9,14 @@ import pytest
from docker.errors import NotFound from docker.errors import NotFound
from .. import mock from .. import mock
from ..helpers import build_config from ..helpers import build_config as load_config
from .testcases import DockerClientTestCase from .testcases import DockerClientTestCase
from compose.config import config from compose.config import config
from compose.config import ConfigurationError from compose.config import ConfigurationError
from compose.config import types
from compose.config.config import V2_0 from compose.config.config import V2_0
from compose.config.config import V2_1 from compose.config.config import V2_1
from compose.config.config import V3_1
from compose.config.types import VolumeFromSpec from compose.config.types import VolumeFromSpec
from compose.config.types import VolumeSpec from compose.config.types import VolumeSpec
from compose.const import LABEL_PROJECT from compose.const import LABEL_PROJECT
@ -26,6 +29,16 @@ from compose.project import ProjectError
from compose.service import ConvergenceStrategy from compose.service import ConvergenceStrategy
from tests.integration.testcases import v2_1_only from tests.integration.testcases import v2_1_only
from tests.integration.testcases import v2_only from tests.integration.testcases import v2_only
from tests.integration.testcases import v3_only
def build_config(**kwargs):
return config.Config(
version=kwargs.get('version'),
services=kwargs.get('services'),
volumes=kwargs.get('volumes'),
networks=kwargs.get('networks'),
secrets=kwargs.get('secrets'))
class ProjectTest(DockerClientTestCase): class ProjectTest(DockerClientTestCase):
@ -70,7 +83,7 @@ class ProjectTest(DockerClientTestCase):
def test_volumes_from_service(self): def test_volumes_from_service(self):
project = Project.from_config( project = Project.from_config(
name='composetest', name='composetest',
config_data=build_config({ config_data=load_config({
'data': { 'data': {
'image': 'busybox:latest', 'image': 'busybox:latest',
'volumes': ['/var/data'], 'volumes': ['/var/data'],
@ -96,7 +109,7 @@ class ProjectTest(DockerClientTestCase):
) )
project = Project.from_config( project = Project.from_config(
name='composetest', name='composetest',
config_data=build_config({ config_data=load_config({
'db': { 'db': {
'image': 'busybox:latest', 'image': 'busybox:latest',
'volumes_from': ['composetest_data_container'], 'volumes_from': ['composetest_data_container'],
@ -112,7 +125,7 @@ class ProjectTest(DockerClientTestCase):
project = Project.from_config( project = Project.from_config(
name='composetest', name='composetest',
client=self.client, client=self.client,
config_data=build_config({ config_data=load_config({
'version': V2_0, 'version': V2_0,
'services': { 'services': {
'net': { 'net': {
@ -139,7 +152,7 @@ class ProjectTest(DockerClientTestCase):
def get_project(): def get_project():
return Project.from_config( return Project.from_config(
name='composetest', name='composetest',
config_data=build_config({ config_data=load_config({
'version': V2_0, 'version': V2_0,
'services': { 'services': {
'web': { 'web': {
@ -174,7 +187,7 @@ class ProjectTest(DockerClientTestCase):
def test_net_from_service_v1(self): def test_net_from_service_v1(self):
project = Project.from_config( project = Project.from_config(
name='composetest', name='composetest',
config_data=build_config({ config_data=load_config({
'net': { 'net': {
'image': 'busybox:latest', 'image': 'busybox:latest',
'command': ["top"] 'command': ["top"]
@ -198,7 +211,7 @@ class ProjectTest(DockerClientTestCase):
def get_project(): def get_project():
return Project.from_config( return Project.from_config(
name='composetest', name='composetest',
config_data=build_config({ config_data=load_config({
'web': { 'web': {
'image': 'busybox:latest', 'image': 'busybox:latest',
'net': 'container:composetest_net_container' 'net': 'container:composetest_net_container'
@ -469,7 +482,7 @@ class ProjectTest(DockerClientTestCase):
def test_project_up_starts_depends(self): def test_project_up_starts_depends(self):
project = Project.from_config( project = Project.from_config(
name='composetest', name='composetest',
config_data=build_config({ config_data=load_config({
'console': { 'console': {
'image': 'busybox:latest', 'image': 'busybox:latest',
'command': ["top"], 'command': ["top"],
@ -504,7 +517,7 @@ class ProjectTest(DockerClientTestCase):
def test_project_up_with_no_deps(self): def test_project_up_with_no_deps(self):
project = Project.from_config( project = Project.from_config(
name='composetest', name='composetest',
config_data=build_config({ config_data=load_config({
'console': { 'console': {
'image': 'busybox:latest', 'image': 'busybox:latest',
'command': ["top"], 'command': ["top"],
@ -564,7 +577,7 @@ class ProjectTest(DockerClientTestCase):
@v2_only() @v2_only()
def test_project_up_networks(self): def test_project_up_networks(self):
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -576,7 +589,6 @@ class ProjectTest(DockerClientTestCase):
'baz': {'aliases': ['extra']}, 'baz': {'aliases': ['extra']},
}, },
}], }],
volumes={},
networks={ networks={
'foo': {'driver': 'bridge'}, 'foo': {'driver': 'bridge'},
'bar': {'driver': None}, 'bar': {'driver': None},
@ -610,14 +622,13 @@ class ProjectTest(DockerClientTestCase):
@v2_only() @v2_only()
def test_up_with_ipam_config(self): def test_up_with_ipam_config(self):
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
'image': 'busybox:latest', 'image': 'busybox:latest',
'networks': {'front': None}, 'networks': {'front': None},
}], }],
volumes={},
networks={ networks={
'front': { 'front': {
'driver': 'bridge', 'driver': 'bridge',
@ -671,7 +682,7 @@ class ProjectTest(DockerClientTestCase):
@v2_only() @v2_only()
def test_up_with_network_static_addresses(self): def test_up_with_network_static_addresses(self):
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -684,7 +695,6 @@ class ProjectTest(DockerClientTestCase):
} }
}, },
}], }],
volumes={},
networks={ networks={
'static_test': { 'static_test': {
'driver': 'bridge', 'driver': 'bridge',
@ -726,7 +736,7 @@ class ProjectTest(DockerClientTestCase):
@v2_1_only() @v2_1_only()
def test_up_with_enable_ipv6(self): def test_up_with_enable_ipv6(self):
self.require_api_version('1.23') self.require_api_version('1.23')
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -738,7 +748,6 @@ class ProjectTest(DockerClientTestCase):
} }
}, },
}], }],
volumes={},
networks={ networks={
'static_test': { 'static_test': {
'driver': 'bridge', 'driver': 'bridge',
@ -770,7 +779,7 @@ class ProjectTest(DockerClientTestCase):
@v2_only() @v2_only()
def test_up_with_network_static_addresses_missing_subnet(self): def test_up_with_network_static_addresses_missing_subnet(self):
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -782,7 +791,6 @@ class ProjectTest(DockerClientTestCase):
} }
}, },
}], }],
volumes={},
networks={ networks={
'static_test': { 'static_test': {
'driver': 'bridge', 'driver': 'bridge',
@ -807,7 +815,7 @@ class ProjectTest(DockerClientTestCase):
@v2_1_only() @v2_1_only()
def test_up_with_network_link_local_ips(self): def test_up_with_network_link_local_ips(self):
config_data = config.Config( config_data = build_config(
version=V2_1, version=V2_1,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -818,7 +826,6 @@ class ProjectTest(DockerClientTestCase):
} }
} }
}], }],
volumes={},
networks={ networks={
'linklocaltest': {'driver': 'bridge'} 'linklocaltest': {'driver': 'bridge'}
} }
@ -844,15 +851,13 @@ class ProjectTest(DockerClientTestCase):
@v2_1_only() @v2_1_only()
def test_up_with_isolation(self): def test_up_with_isolation(self):
self.require_api_version('1.24') self.require_api_version('1.24')
config_data = config.Config( config_data = build_config(
version=V2_1, version=V2_1,
services=[{ services=[{
'name': 'web', 'name': 'web',
'image': 'busybox:latest', 'image': 'busybox:latest',
'isolation': 'default' 'isolation': 'default'
}], }],
volumes={},
networks={}
) )
project = Project.from_config( project = Project.from_config(
client=self.client, client=self.client,
@ -866,15 +871,13 @@ class ProjectTest(DockerClientTestCase):
@v2_1_only() @v2_1_only()
def test_up_with_invalid_isolation(self): def test_up_with_invalid_isolation(self):
self.require_api_version('1.24') self.require_api_version('1.24')
config_data = config.Config( config_data = build_config(
version=V2_1, version=V2_1,
services=[{ services=[{
'name': 'web', 'name': 'web',
'image': 'busybox:latest', 'image': 'busybox:latest',
'isolation': 'foobar' 'isolation': 'foobar'
}], }],
volumes={},
networks={}
) )
project = Project.from_config( project = Project.from_config(
client=self.client, client=self.client,
@ -887,14 +890,13 @@ class ProjectTest(DockerClientTestCase):
@v2_only() @v2_only()
def test_project_up_with_network_internal(self): def test_project_up_with_network_internal(self):
self.require_api_version('1.23') self.require_api_version('1.23')
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
'image': 'busybox:latest', 'image': 'busybox:latest',
'networks': {'internal': None}, 'networks': {'internal': None},
}], }],
volumes={},
networks={ networks={
'internal': {'driver': 'bridge', 'internal': True}, 'internal': {'driver': 'bridge', 'internal': True},
}, },
@ -917,14 +919,13 @@ class ProjectTest(DockerClientTestCase):
network_name = 'network_with_label' network_name = 'network_with_label'
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
'image': 'busybox:latest', 'image': 'busybox:latest',
'networks': {network_name: None} 'networks': {network_name: None}
}], }],
volumes={},
networks={ networks={
network_name: {'labels': {'label_key': 'label_val'}} network_name: {'labels': {'label_key': 'label_val'}}
} }
@ -951,7 +952,7 @@ class ProjectTest(DockerClientTestCase):
def test_project_up_volumes(self): def test_project_up_volumes(self):
vol_name = '{0:x}'.format(random.getrandbits(32)) vol_name = '{0:x}'.format(random.getrandbits(32))
full_vol_name = 'composetest_{0}'.format(vol_name) full_vol_name = 'composetest_{0}'.format(vol_name)
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -959,7 +960,6 @@ class ProjectTest(DockerClientTestCase):
'command': 'top' 'command': 'top'
}], }],
volumes={vol_name: {'driver': 'local'}}, volumes={vol_name: {'driver': 'local'}},
networks={},
) )
project = Project.from_config( project = Project.from_config(
@ -979,7 +979,7 @@ class ProjectTest(DockerClientTestCase):
volume_name = 'volume_with_label' volume_name = 'volume_with_label'
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -993,7 +993,6 @@ class ProjectTest(DockerClientTestCase):
} }
} }
}, },
networks={},
) )
project = Project.from_config( project = Project.from_config(
@ -1106,7 +1105,7 @@ class ProjectTest(DockerClientTestCase):
def test_initialize_volumes(self): def test_initialize_volumes(self):
vol_name = '{0:x}'.format(random.getrandbits(32)) vol_name = '{0:x}'.format(random.getrandbits(32))
full_vol_name = 'composetest_{0}'.format(vol_name) full_vol_name = 'composetest_{0}'.format(vol_name)
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -1114,7 +1113,6 @@ class ProjectTest(DockerClientTestCase):
'command': 'top' 'command': 'top'
}], }],
volumes={vol_name: {}}, volumes={vol_name: {}},
networks={},
) )
project = Project.from_config( project = Project.from_config(
@ -1124,14 +1122,14 @@ class ProjectTest(DockerClientTestCase):
project.volumes.initialize() project.volumes.initialize()
volume_data = self.client.inspect_volume(full_vol_name) volume_data = self.client.inspect_volume(full_vol_name)
self.assertEqual(volume_data['Name'], full_vol_name) assert volume_data['Name'] == full_vol_name
self.assertEqual(volume_data['Driver'], 'local') assert volume_data['Driver'] == 'local'
@v2_only() @v2_only()
def test_project_up_implicit_volume_driver(self): def test_project_up_implicit_volume_driver(self):
vol_name = '{0:x}'.format(random.getrandbits(32)) vol_name = '{0:x}'.format(random.getrandbits(32))
full_vol_name = 'composetest_{0}'.format(vol_name) full_vol_name = 'composetest_{0}'.format(vol_name)
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -1139,7 +1137,6 @@ class ProjectTest(DockerClientTestCase):
'command': 'top' 'command': 'top'
}], }],
volumes={vol_name: {}}, volumes={vol_name: {}},
networks={},
) )
project = Project.from_config( project = Project.from_config(
@ -1152,11 +1149,47 @@ class ProjectTest(DockerClientTestCase):
self.assertEqual(volume_data['Name'], full_vol_name) self.assertEqual(volume_data['Name'], full_vol_name)
self.assertEqual(volume_data['Driver'], 'local') self.assertEqual(volume_data['Driver'], 'local')
@v3_only()
def test_project_up_with_secrets(self):
create_host_file(self.client, os.path.abspath('tests/fixtures/secrets/default'))
config_data = build_config(
version=V3_1,
services=[{
'name': 'web',
'image': 'busybox:latest',
'command': 'cat /run/secrets/special',
'secrets': [
types.ServiceSecret.parse({'source': 'super', 'target': 'special'}),
],
}],
secrets={
'super': {
'file': os.path.abspath('tests/fixtures/secrets/default'),
},
},
)
project = Project.from_config(
client=self.client,
name='composetest',
config_data=config_data,
)
project.up()
project.stop()
containers = project.containers(stopped=True)
assert len(containers) == 1
container, = containers
output = container.logs()
assert output == b"This is the secret\n"
@v2_only() @v2_only()
def test_initialize_volumes_invalid_volume_driver(self): def test_initialize_volumes_invalid_volume_driver(self):
vol_name = '{0:x}'.format(random.getrandbits(32)) vol_name = '{0:x}'.format(random.getrandbits(32))
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -1164,7 +1197,6 @@ class ProjectTest(DockerClientTestCase):
'command': 'top' 'command': 'top'
}], }],
volumes={vol_name: {'driver': 'foobar'}}, volumes={vol_name: {'driver': 'foobar'}},
networks={},
) )
project = Project.from_config( project = Project.from_config(
@ -1179,7 +1211,7 @@ class ProjectTest(DockerClientTestCase):
vol_name = '{0:x}'.format(random.getrandbits(32)) vol_name = '{0:x}'.format(random.getrandbits(32))
full_vol_name = 'composetest_{0}'.format(vol_name) full_vol_name = 'composetest_{0}'.format(vol_name)
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -1187,7 +1219,6 @@ class ProjectTest(DockerClientTestCase):
'command': 'top' 'command': 'top'
}], }],
volumes={vol_name: {'driver': 'local'}}, volumes={vol_name: {'driver': 'local'}},
networks={},
) )
project = Project.from_config( project = Project.from_config(
name='composetest', name='composetest',
@ -1218,7 +1249,7 @@ class ProjectTest(DockerClientTestCase):
vol_name = '{0:x}'.format(random.getrandbits(32)) vol_name = '{0:x}'.format(random.getrandbits(32))
full_vol_name = 'composetest_{0}'.format(vol_name) full_vol_name = 'composetest_{0}'.format(vol_name)
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -1226,7 +1257,6 @@ class ProjectTest(DockerClientTestCase):
'command': 'top' 'command': 'top'
}], }],
volumes={vol_name: {'driver': 'local'}}, volumes={vol_name: {'driver': 'local'}},
networks={},
) )
project = Project.from_config( project = Project.from_config(
name='composetest', name='composetest',
@ -1257,7 +1287,7 @@ class ProjectTest(DockerClientTestCase):
vol_name = 'composetest_{0:x}'.format(random.getrandbits(32)) vol_name = 'composetest_{0:x}'.format(random.getrandbits(32))
full_vol_name = 'composetest_{0}'.format(vol_name) full_vol_name = 'composetest_{0}'.format(vol_name)
self.client.create_volume(vol_name) self.client.create_volume(vol_name)
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -1267,7 +1297,6 @@ class ProjectTest(DockerClientTestCase):
volumes={ volumes={
vol_name: {'external': True, 'external_name': vol_name} vol_name: {'external': True, 'external_name': vol_name}
}, },
networks=None,
) )
project = Project.from_config( project = Project.from_config(
name='composetest', name='composetest',
@ -1282,7 +1311,7 @@ class ProjectTest(DockerClientTestCase):
def test_initialize_volumes_inexistent_external_volume(self): def test_initialize_volumes_inexistent_external_volume(self):
vol_name = '{0:x}'.format(random.getrandbits(32)) vol_name = '{0:x}'.format(random.getrandbits(32))
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -1292,7 +1321,6 @@ class ProjectTest(DockerClientTestCase):
volumes={ volumes={
vol_name: {'external': True, 'external_name': vol_name} vol_name: {'external': True, 'external_name': vol_name}
}, },
networks=None,
) )
project = Project.from_config( project = Project.from_config(
name='composetest', name='composetest',
@ -1349,7 +1377,7 @@ class ProjectTest(DockerClientTestCase):
} }
} }
config_data = build_config(config_dict) config_data = load_config(config_dict)
project = Project.from_config( project = Project.from_config(
name='composetest', config_data=config_data, client=self.client name='composetest', config_data=config_data, client=self.client
) )
@ -1357,7 +1385,7 @@ class ProjectTest(DockerClientTestCase):
config_dict['service2'] = config_dict['service1'] config_dict['service2'] = config_dict['service1']
del config_dict['service1'] del config_dict['service1']
config_data = build_config(config_dict) config_data = load_config(config_dict)
project = Project.from_config( project = Project.from_config(
name='composetest', config_data=config_data, client=self.client name='composetest', config_data=config_data, client=self.client
) )
@ -1402,7 +1430,7 @@ class ProjectTest(DockerClientTestCase):
} }
} }
} }
config_data = build_config(config_dict) config_data = load_config(config_dict)
project = Project.from_config( project = Project.from_config(
name='composetest', config_data=config_data, client=self.client name='composetest', config_data=config_data, client=self.client
) )
@ -1439,11 +1467,11 @@ class ProjectTest(DockerClientTestCase):
} }
} }
} }
config_data = build_config(config_dict) config_data = load_config(config_dict)
project = Project.from_config( project = Project.from_config(
name='composetest', config_data=config_data, client=self.client name='composetest', config_data=config_data, client=self.client
) )
with pytest.raises(HealthCheckFailed): with pytest.raises(ProjectError):
project.up() project.up()
containers = project.containers() containers = project.containers()
assert len(containers) == 1 assert len(containers) == 1
@ -1463,7 +1491,7 @@ class ProjectTest(DockerClientTestCase):
'image': 'busybox:latest', 'image': 'busybox:latest',
'command': 'top', 'command': 'top',
'healthcheck': { 'healthcheck': {
'disabled': True 'disable': True
}, },
}, },
'svc2': { 'svc2': {
@ -1475,11 +1503,11 @@ class ProjectTest(DockerClientTestCase):
} }
} }
} }
config_data = build_config(config_dict) config_data = load_config(config_dict)
project = Project.from_config( project = Project.from_config(
name='composetest', config_data=config_data, client=self.client name='composetest', config_data=config_data, client=self.client
) )
with pytest.raises(NoHealthCheckConfigured): with pytest.raises(ProjectError):
project.up() project.up()
containers = project.containers() containers = project.containers()
assert len(containers) == 1 assert len(containers) == 1
@ -1489,3 +1517,30 @@ class ProjectTest(DockerClientTestCase):
assert 'svc1' in svc2.get_dependency_names() assert 'svc1' in svc2.get_dependency_names()
with pytest.raises(NoHealthCheckConfigured): with pytest.raises(NoHealthCheckConfigured):
svc1.is_healthy() svc1.is_healthy()
def create_host_file(client, filename):
dirname = os.path.dirname(filename)
with open(filename, 'r') as fh:
content = fh.read()
container = client.create_container(
'busybox:latest',
['sh', '-c', 'echo -n "{}" > {}'.format(content, filename)],
volumes={dirname: {}},
host_config=client.create_host_config(
binds={dirname: {'bind': dirname, 'ro': False}},
network_mode='none',
),
)
try:
client.start(container)
exitcode = client.wait(container)
if exitcode != 0:
output = client.logs(container)
raise Exception(
"Container exited with code {}:\n{}".format(exitcode, output))
finally:
client.remove_container(container, force=True)

View file

@ -13,6 +13,7 @@ from compose.config.config import resolve_environment
from compose.config.config import V1 from compose.config.config import V1
from compose.config.config import V2_0 from compose.config.config import V2_0
from compose.config.config import V2_1 from compose.config.config import V2_1
from compose.config.config import V3_0
from compose.config.environment import Environment from compose.config.environment import Environment
from compose.const import API_VERSIONS from compose.const import API_VERSIONS
from compose.const import LABEL_PROJECT from compose.const import LABEL_PROJECT
@ -36,21 +37,24 @@ def get_links(container):
def engine_max_version(): def engine_max_version():
if 'DOCKER_VERSION' not in os.environ: if 'DOCKER_VERSION' not in os.environ:
return V2_1 return V3_0
version = os.environ['DOCKER_VERSION'].partition('-')[0] version = os.environ['DOCKER_VERSION'].partition('-')[0]
if version_lt(version, '1.10'): if version_lt(version, '1.10'):
return V1 return V1
elif version_lt(version, '1.12'): if version_lt(version, '1.12'):
return V2_0 return V2_0
return V2_1 if version_lt(version, '1.13'):
return V2_1
return V3_0
def build_version_required_decorator(ignored_versions): def build_version_required_decorator(ignored_versions):
def decorator(f): def decorator(f):
@functools.wraps(f) @functools.wraps(f)
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
if engine_max_version() in ignored_versions: max_version = engine_max_version()
skip("Engine version is too low") if max_version in ignored_versions:
skip("Engine version %s is too low" % max_version)
return return
return f(self, *args, **kwargs) return f(self, *args, **kwargs)
return wrapper return wrapper

View file

@ -77,7 +77,8 @@ def test_to_bundle():
version=2, version=2,
services=services, services=services,
volumes={'special': {}}, volumes={'special': {}},
networks={'extra': {}}) networks={'extra': {}},
secrets={})
with mock.patch('compose.bundle.log.warn', autospec=True) as mock_log: with mock.patch('compose.bundle.log.warn', autospec=True) as mock_log:
output = bundle.to_bundle(config, image_digests) output = bundle.to_bundle(config, image_digests)

View file

@ -184,7 +184,7 @@ class CLITestCase(unittest.TestCase):
mock_client.create_host_config.call_args[1].get('restart_policy') mock_client.create_host_config.call_args[1].get('restart_policy')
) )
def test_command_manula_and_service_ports_together(self): def test_command_manual_and_service_ports_together(self):
project = Project.from_config( project = Project.from_config(
name='composetest', name='composetest',
client=None, client=None,

View file

@ -13,15 +13,19 @@ import pytest
from ...helpers import build_config_details from ...helpers import build_config_details
from compose.config import config from compose.config import config
from compose.config import types
from compose.config.config import resolve_build_args from compose.config.config import resolve_build_args
from compose.config.config import resolve_environment from compose.config.config import resolve_environment
from compose.config.config import V1 from compose.config.config import V1
from compose.config.config import V2_0 from compose.config.config import V2_0
from compose.config.config import V2_1 from compose.config.config import V2_1
from compose.config.config import V3_0 from compose.config.config import V3_0
from compose.config.config import V3_1
from compose.config.environment import Environment from compose.config.environment import Environment
from compose.config.errors import ConfigurationError from compose.config.errors import ConfigurationError
from compose.config.errors import VERSION_EXPLANATION from compose.config.errors import VERSION_EXPLANATION
from compose.config.serialize import denormalize_service_dict
from compose.config.serialize import serialize_ns_time_value
from compose.config.types import VolumeSpec from compose.config.types import VolumeSpec
from compose.const import IS_WINDOWS_PLATFORM from compose.const import IS_WINDOWS_PLATFORM
from compose.utils import nanoseconds_from_time_seconds from compose.utils import nanoseconds_from_time_seconds
@ -50,6 +54,10 @@ def service_sort(services):
return sorted(services, key=itemgetter('name')) return sorted(services, key=itemgetter('name'))
def secret_sort(secrets):
return sorted(secrets, key=itemgetter('source'))
class ConfigTest(unittest.TestCase): class ConfigTest(unittest.TestCase):
def test_load(self): def test_load(self):
service_dicts = config.load( service_dicts = config.load(
@ -166,6 +174,9 @@ class ConfigTest(unittest.TestCase):
cfg = config.load(build_config_details({'version': version})) cfg = config.load(build_config_details({'version': version}))
assert cfg.version == V3_0 assert cfg.version == V3_0
cfg = config.load(build_config_details({'version': '3.1'}))
assert cfg.version == V3_1
def test_v1_file_version(self): def test_v1_file_version(self):
cfg = config.load(build_config_details({'web': {'image': 'busybox'}})) cfg = config.load(build_config_details({'web': {'image': 'busybox'}}))
assert cfg.version == V1 assert cfg.version == V1
@ -1712,6 +1723,90 @@ class ConfigTest(unittest.TestCase):
} }
} }
def test_merge_depends_on_no_override(self):
base = {
'image': 'busybox',
'depends_on': {
'app1': {'condition': 'service_started'},
'app2': {'condition': 'service_healthy'}
}
}
override = {}
actual = config.merge_service_dicts(base, override, V2_1)
assert actual == base
def test_merge_depends_on_mixed_syntax(self):
base = {
'image': 'busybox',
'depends_on': {
'app1': {'condition': 'service_started'},
'app2': {'condition': 'service_healthy'}
}
}
override = {
'depends_on': ['app3']
}
actual = config.merge_service_dicts(base, override, V2_1)
assert actual == {
'image': 'busybox',
'depends_on': {
'app1': {'condition': 'service_started'},
'app2': {'condition': 'service_healthy'},
'app3': {'condition': 'service_started'}
}
}
def test_merge_pid(self):
# Regression: https://github.com/docker/compose/issues/4184
base = {
'image': 'busybox',
'pid': 'host'
}
override = {
'labels': {'com.docker.compose.test': 'yes'}
}
actual = config.merge_service_dicts(base, override, V2_0)
assert actual == {
'image': 'busybox',
'pid': 'host',
'labels': {'com.docker.compose.test': 'yes'}
}
def test_merge_different_secrets(self):
base = {
'image': 'busybox',
'secrets': [
{'source': 'src.txt'}
]
}
override = {'secrets': ['other-src.txt']}
actual = config.merge_service_dicts(base, override, V3_1)
assert secret_sort(actual['secrets']) == secret_sort([
{'source': 'src.txt'},
{'source': 'other-src.txt'}
])
def test_merge_secrets_override(self):
base = {
'image': 'busybox',
'secrets': ['src.txt'],
}
override = {
'secrets': [
{
'source': 'src.txt',
'target': 'data.txt',
'mode': 0o400
}
]
}
actual = config.merge_service_dicts(base, override, V3_1)
assert actual['secrets'] == override['secrets']
def test_external_volume_config(self): def test_external_volume_config(self):
config_details = build_config_details({ config_details = build_config_details({
'version': '2', 'version': '2',
@ -1791,6 +1886,91 @@ class ConfigTest(unittest.TestCase):
config.load(config_details) config.load(config_details)
assert 'has neither an image nor a build context' in exc.exconly() assert 'has neither an image nor a build context' in exc.exconly()
def test_load_secrets(self):
base_file = config.ConfigFile(
'base.yaml',
{
'version': '3.1',
'services': {
'web': {
'image': 'example/web',
'secrets': [
'one',
{
'source': 'source',
'target': 'target',
'uid': '100',
'gid': '200',
'mode': 0o777,
},
],
},
},
'secrets': {
'one': {'file': 'secret.txt'},
},
})
details = config.ConfigDetails('.', [base_file])
service_dicts = config.load(details).services
expected = [
{
'name': 'web',
'image': 'example/web',
'secrets': [
types.ServiceSecret('one', None, None, None, None),
types.ServiceSecret('source', 'target', '100', '200', 0o777),
],
},
]
assert service_sort(service_dicts) == service_sort(expected)
def test_load_secrets_multi_file(self):
base_file = config.ConfigFile(
'base.yaml',
{
'version': '3.1',
'services': {
'web': {
'image': 'example/web',
'secrets': ['one'],
},
},
'secrets': {
'one': {'file': 'secret.txt'},
},
})
override_file = config.ConfigFile(
'base.yaml',
{
'version': '3.1',
'services': {
'web': {
'secrets': [
{
'source': 'source',
'target': 'target',
'uid': '100',
'gid': '200',
'mode': 0o777,
},
],
},
},
})
details = config.ConfigDetails('.', [base_file, override_file])
service_dicts = config.load(details).services
expected = [
{
'name': 'web',
'image': 'example/web',
'secrets': [
types.ServiceSecret('one', None, None, None, None),
types.ServiceSecret('source', 'target', '100', '200', 0o777),
],
},
]
assert service_sort(service_dicts) == service_sort(expected)
class NetworkModeTest(unittest.TestCase): class NetworkModeTest(unittest.TestCase):
def test_network_mode_standard(self): def test_network_mode_standard(self):
@ -3062,6 +3242,19 @@ class ExtendsTest(unittest.TestCase):
'other': {'condition': 'service_started'} 'other': {'condition': 'service_started'}
} }
def test_extends_with_healthcheck(self):
service_dicts = load_from_filename('tests/fixtures/extends/healthcheck-2.yml')
assert service_sort(service_dicts) == [{
'name': 'demo',
'image': 'foobar:latest',
'healthcheck': {
'test': ['CMD', '/health.sh'],
'interval': 10000000000,
'timeout': 5000000000,
'retries': 36,
}
}]
@pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash') @pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash')
class ExpandPathTest(unittest.TestCase): class ExpandPathTest(unittest.TestCase):
@ -3269,3 +3462,89 @@ def get_config_filename_for_files(filenames, subdir=None):
return os.path.basename(filename) return os.path.basename(filename)
finally: finally:
shutil.rmtree(project_dir) shutil.rmtree(project_dir)
class SerializeTest(unittest.TestCase):
def test_denormalize_depends_on_v3(self):
service_dict = {
'image': 'busybox',
'command': 'true',
'depends_on': {
'service2': {'condition': 'service_started'},
'service3': {'condition': 'service_started'},
}
}
assert denormalize_service_dict(service_dict, V3_0) == {
'image': 'busybox',
'command': 'true',
'depends_on': ['service2', 'service3']
}
def test_denormalize_depends_on_v2_1(self):
service_dict = {
'image': 'busybox',
'command': 'true',
'depends_on': {
'service2': {'condition': 'service_started'},
'service3': {'condition': 'service_started'},
}
}
assert denormalize_service_dict(service_dict, V2_1) == service_dict
def test_serialize_time(self):
data = {
9: '9ns',
9000: '9us',
9000000: '9ms',
90000000: '90ms',
900000000: '900ms',
999999999: '999999999ns',
1000000000: '1s',
60000000000: '1m',
60000000001: '60000000001ns',
9000000000000: '150m',
90000000000000: '25h',
}
for k, v in data.items():
assert serialize_ns_time_value(k) == v
def test_denormalize_healthcheck(self):
service_dict = {
'image': 'test',
'healthcheck': {
'test': 'exit 1',
'interval': '1m40s',
'timeout': '30s',
'retries': 5
}
}
processed_service = config.process_service(config.ServiceConfig(
'.', 'test', 'test', service_dict
))
denormalized_service = denormalize_service_dict(processed_service, V2_1)
assert denormalized_service['healthcheck']['interval'] == '100s'
assert denormalized_service['healthcheck']['timeout'] == '30s'
def test_denormalize_secrets(self):
service_dict = {
'name': 'web',
'image': 'example/web',
'secrets': [
types.ServiceSecret('one', None, None, None, None),
types.ServiceSecret('source', 'target', '100', '200', 0o777),
],
}
denormalized_service = denormalize_service_dict(service_dict, V3_1)
assert secret_sort(denormalized_service['secrets']) == secret_sort([
{'source': 'one'},
{
'source': 'source',
'target': 'target',
'uid': '100',
'gid': '200',
'mode': 0o777,
},
])

View file

@ -0,0 +1,40 @@
# encoding: utf-8
from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
from compose.config.environment import Environment
from tests import unittest
class EnvironmentTest(unittest.TestCase):
def test_get_simple(self):
env = Environment({
'FOO': 'bar',
'BAR': '1',
'BAZ': ''
})
assert env.get('FOO') == 'bar'
assert env.get('BAR') == '1'
assert env.get('BAZ') == ''
def test_get_undefined(self):
env = Environment({
'FOO': 'bar'
})
assert env.get('FOOBAR') is None
def test_get_boolean(self):
env = Environment({
'FOO': '',
'BAR': '0',
'BAZ': 'FALSE',
'FOOBAR': 'true',
})
assert env.get_boolean('FOO') is False
assert env.get_boolean('BAR') is False
assert env.get_boolean('BAZ') is False
assert env.get_boolean('FOOBAR') is True
assert env.get_boolean('UNDEFINED') is False

View file

@ -36,6 +36,7 @@ class ProjectTest(unittest.TestCase):
], ],
networks=None, networks=None,
volumes=None, volumes=None,
secrets=None,
) )
project = Project.from_config( project = Project.from_config(
name='composetest', name='composetest',
@ -64,6 +65,7 @@ class ProjectTest(unittest.TestCase):
], ],
networks=None, networks=None,
volumes=None, volumes=None,
secrets=None,
) )
project = Project.from_config('composetest', config, None) project = Project.from_config('composetest', config, None)
self.assertEqual(len(project.services), 2) self.assertEqual(len(project.services), 2)
@ -170,6 +172,7 @@ class ProjectTest(unittest.TestCase):
}], }],
networks=None, networks=None,
volumes=None, volumes=None,
secrets=None,
), ),
) )
assert project.get_service('test')._get_volumes_from() == [container_id + ":rw"] assert project.get_service('test')._get_volumes_from() == [container_id + ":rw"]
@ -202,6 +205,7 @@ class ProjectTest(unittest.TestCase):
], ],
networks=None, networks=None,
volumes=None, volumes=None,
secrets=None,
), ),
) )
assert project.get_service('test')._get_volumes_from() == [container_name + ":rw"] assert project.get_service('test')._get_volumes_from() == [container_name + ":rw"]
@ -227,6 +231,7 @@ class ProjectTest(unittest.TestCase):
], ],
networks=None, networks=None,
volumes=None, volumes=None,
secrets=None,
), ),
) )
with mock.patch.object(Service, 'containers') as mock_return: with mock.patch.object(Service, 'containers') as mock_return:
@ -360,6 +365,7 @@ class ProjectTest(unittest.TestCase):
], ],
networks=None, networks=None,
volumes=None, volumes=None,
secrets=None,
), ),
) )
service = project.get_service('test') service = project.get_service('test')
@ -384,6 +390,7 @@ class ProjectTest(unittest.TestCase):
], ],
networks=None, networks=None,
volumes=None, volumes=None,
secrets=None,
), ),
) )
service = project.get_service('test') service = project.get_service('test')
@ -417,6 +424,7 @@ class ProjectTest(unittest.TestCase):
], ],
networks=None, networks=None,
volumes=None, volumes=None,
secrets=None,
), ),
) )
@ -437,6 +445,7 @@ class ProjectTest(unittest.TestCase):
], ],
networks=None, networks=None,
volumes=None, volumes=None,
secrets=None,
), ),
) )
@ -457,6 +466,7 @@ class ProjectTest(unittest.TestCase):
], ],
networks={'custom': {}}, networks={'custom': {}},
volumes=None, volumes=None,
secrets=None,
), ),
) )
@ -487,6 +497,7 @@ class ProjectTest(unittest.TestCase):
}], }],
networks=None, networks=None,
volumes=None, volumes=None,
secrets=None,
), ),
) )
self.assertEqual([c.id for c in project.containers()], ['1']) self.assertEqual([c.id for c in project.containers()], ['1'])
@ -503,6 +514,7 @@ class ProjectTest(unittest.TestCase):
}], }],
networks={'default': {}}, networks={'default': {}},
volumes={'data': {}}, volumes={'data': {}},
secrets=None,
), ),
) )
self.mock_client.remove_network.side_effect = NotFound(None, None, 'oops') self.mock_client.remove_network.side_effect = NotFound(None, None, 'oops')