Compare commits

..

388 commits

Author SHA1 Message Date
Daniel Nephin
a75c16cb1b Bump 1.6.1
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-23 12:08:56 -08:00
Mary Anthony
e08409f18d Updating Dockerfile
Signed-off-by: Mary Anthony <mary@docker.com>
2016-02-23 12:08:56 -08:00
Daniel Nephin
9d7dbe3857 Make environment variables without a value the same as docker-cli.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-23 11:58:16 -08:00
Daniel Nephin
bcd5286cd3 Revert "Change special case from '_', None to ()"
This reverts commit 677c50650c.

Revert "Modify service_test.py::ServiceTest::test_resolve_env to reflect new behavior"

This reverts commit 0019037712.

Revert "Mangle the tests. They pass for better or worse!"

This reverts commit 7ab9509ce6.

Revert "If an env var is passthrough but not defined on the host don't set it."

This reverts commit 6540efb3d3.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-23 11:58:16 -08:00
Daniel Nephin
daebf74d6c Stop other containers if the flag is set.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-23 11:52:51 -08:00
Richard Bann
155d813606 Add failing test for --abort-on-container-exit
Handle --abort-on-container-exit. Fixes #2940

Signed-off-by: Richard Bann <richardbann@gmail.com>
2016-02-23 11:52:51 -08:00
Daniel Nephin
73c2f8ee37 Fix warning about boolean values.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-23 11:52:51 -08:00
Joffrey F
4aae2c3b7b Use docker-py 1.7.1
Signed-off-by: Joffrey F <joffrey@docker.com>
2016-02-23 11:52:51 -08:00
Daniel Nephin
b79ad5f966 Fix validation message when there are multiple ested oneOf validations.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-23 11:52:51 -08:00
Victoria Bialas
f8e3c46fbb copyedit to make show as file format
Signed-off-by: Victoria Bialas <victoria.bialas@docker.com>
2016-02-23 11:52:51 -08:00
Victoria Bialas
f7c923062d corrected description of network aliases, added real-world example per #2907
Signed-off-by: Victoria Bialas <victoria.bialas@docker.com>
2016-02-23 11:52:51 -08:00
Joffrey F
61906ac2ff Update documentation for volume_driver option.
Signed-off-by: Joffrey F <joffrey@docker.com>
2016-02-23 11:52:51 -08:00
Daniel Nephin
ea8032c115 Make config validation error messages more consistent.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-23 11:52:51 -08:00
Daniel Nephin
7d22809ef4 Validate that each section of the config is a mapping before running interpolation.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-23 11:52:51 -08:00
Anthon van der Neut
cfda9d844e for 1.6.0 the version value needs to be a string
After conversion a file would immediately not load in docker-compose 1.6.0 with the message:

  ERROR: Version in "./converted.yml" is invalid - it should be a string.

Signed-off-by: Anthon van der Neut anthon@mnt.org
Signed-off-by: Anthon van der Neut <a.van.der.neut@ruamel.eu>
2016-02-23 11:52:51 -08:00
Joffrey F
1e29ad9fc7 Apply driver_opts processing to network configs
Signed-off-by: Joffrey F <joffrey@docker.com>
2016-02-23 11:52:51 -08:00
Joffrey F
fcf78fe3de Constraint build argument types. Numbers are cast into strings
Numerical driver_opts are also valid and typecast into strings.
Additional config tests.

Signed-off-by: Joffrey F <joffrey@docker.com>
2016-02-23 11:52:51 -08:00
Daniel Nephin
8d7b1e9047 Update guides to use v2 config format.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-23 11:52:51 -08:00
Daniel Nephin
6d2aa80435 Update link to docker volume create docs.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-23 11:52:51 -08:00
Daniel Nephin
13ec3d0217 Fix copying of volumes by using the name of the volume instead of the host path.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-23 11:52:51 -08:00
Anthon van der Neut
f59fef09a6 reset colors after warning
If a warning is shown, and you happen to have no color setting in your (bash) prompt, the \033[37m setting, stays active. With the message hardly readable (light grey on my default light yellow background), that means the prompt is barely visible and you need to do `tput reset`.
Would probably be better if the background color was set as well in case you have dark on light theme by default in your terminal.

Signed-off-by: Anthon van der Neut <a.van.der.neut@ruamel.eu>
2016-02-23 11:52:51 -08:00
Chia-liang Kao
f0a8c65b05 Quote argv as they are
Signed-off-by: Chia-liang Kao <clkao@clkao.org>
2016-02-23 11:52:51 -08:00
Chia-liang Kao
674e541cf7 Detect -t and -i separately
Signed-off-by: Chia-liang Kao <clkao@clkao.org>
2016-02-23 11:52:51 -08:00
Chia-liang Kao
ed5fedf516 Don't mount pwd if it is /
Signed-off-by: Chia-liang Kao <clkao@clkao.org>
2016-02-23 11:52:51 -08:00
Joffrey F
654b3710f7 Use modern set notation in _get_aliases
Signed-off-by: Joffrey F <joffrey@docker.com>
2016-02-23 11:52:51 -08:00
Joffrey F
f4a22b94ed Handle mismatched network formats in config files
Signed-off-by: Joffrey F <joffrey@docker.com>
2016-02-23 11:52:51 -08:00
Joffrey F
e5689afe4c Network aliases are now part of the network dictionary
Signed-off-by: Joffrey F <joffrey@docker.com>
2016-02-23 11:52:51 -08:00
Joffrey F
42cb719b52 Add v2_only decorator to network aliases test
Signed-off-by: Joffrey F <joffrey@docker.com>
2016-02-23 11:52:51 -08:00
Joffrey F
6ac6860dda Fix network list serialization in py3
Signed-off-by: Joffrey F <joffrey@docker.com>
2016-02-23 11:52:51 -08:00
Joffrey F
353da73eab Document network_aliases config
Signed-off-by: Joffrey F <joffrey@docker.com>
2016-02-23 11:52:51 -08:00
Joffrey F
c686be8fd3 Test network_aliases feature
Signed-off-by: Joffrey F <joffrey@docker.com>
2016-02-23 11:52:51 -08:00
Joffrey F
2f7a77e954 Allow user to specify custom network aliases
Signed-off-by: Joffrey F <joffrey@docker.com>
2016-02-23 11:52:51 -08:00
Joffrey F
d78ea85301 driver_opts can only be of type string
Signed-off-by: Joffrey F <joffrey@docker.com>
2016-02-23 11:52:51 -08:00
Daniel Nephin
7c95c733a9 Only set a container affinity if there are volumes to copy over.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-23 11:52:51 -08:00
Joffrey F
dd9a8d6eee Bring up all dependencies when running a single service.
Added test for running a depends_on service

Signed-off-by: Joffrey F <joffrey@docker.com>
2016-02-23 11:52:51 -08:00
Daniel Nephin
4f7c950ca8 Upgrade pyinstaller.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-23 11:52:51 -08:00
Joffrey F
a1d6e3b9e3 Add logging when initializing a volume.
Signed-off-by: Joffrey F <joffrey@docker.com>
2016-02-23 11:52:51 -08:00
Joffrey F
3eac70a9d3 Detailed error message when daemon version is too old.
Signed-off-by: Joffrey F <joffrey@docker.com>
2016-02-23 11:52:51 -08:00
Manuel Kaufmann
c7687592ff Typo fixed
Signed-off-by: Manuel Kaufmann <humitos@gmail.com>
2016-02-23 11:52:51 -08:00
Daniel Nephin
155efd28fa Merge build.args when merging services.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-23 11:52:51 -08:00
Daniel Nephin
2ced83e3d9 Fix build section without context.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-23 11:52:51 -08:00
Yohan Graterol
cea7911f56 Typo into the doc with networks in yaml
Signed-off-by: Yohan Graterol <yohangraterol92@gmail.com>
2016-02-23 11:52:51 -08:00
Daniel Nephin
db12794b1c Fix upgrading url.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-23 11:52:51 -08:00
Daniel Nephin
deeca57a0d Use 12 characters for the short id to match docker and fix backwards compatibility.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-23 11:52:51 -08:00
cr7pt0gr4ph7
edcbe2eb4d Simplify unit tests in config/config_test.py by using class variables instead of methods for parametrizing tests.
Signed-off-by: cr7pt0gr4ph7 <cr7pt0gr4ph7@gmail.com>
2016-02-23 11:52:51 -08:00
Lukas Waslowski
ad00f3dd21 Handle the 'network_mode' key when merging multiple compose files.
Fixes docker/compose#2840.

Signed-off-by: Lukas Waslowski <cr7pt0gr4ph7@gmail.com>
2016-02-23 11:52:51 -08:00
Lukas Waslowski
c77a8cfe3b Correctly merge the 'services/<service>/networks' key in the case of multiple compose files.
Fixes docker/compose#2839.

Signed-off-by: Lukas Waslowski <cr7pt0gr4ph7@gmail.com>
2016-02-23 11:52:51 -08:00
Lukas Waslowski
8548b75582 Separate MergePortsTest from MergeListsTest and add MergeNetworksTest.
Signed-off-by: Lukas Waslowski <cr7pt0gr4ph7@gmail.com>
2016-02-23 11:52:51 -08:00
Michael Käufl
c1be49ad53 Used absolute links in readme
This prevents links being broken on pypi
(e.g. https://pypi.python.org/pypi/docs/index.md#features)

Signed-off-by: Michael Käufl <docker-compose@c.michael-kaeufl.de>
2016-02-23 11:52:51 -08:00
Aanand Prasad
a716bdc482 Update Swarm integration guide and make it an official part of the docs
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-02-23 11:52:51 -08:00
jrabbit
abec6f5891 Change special case from '_', None to ()
Signed-off-by: jrabbit <jackjrabbit@gmail.com>
2016-02-23 11:52:51 -08:00
jrabbit
c195915263 Modify service_test.py::ServiceTest::test_resolve_env to reflect new behavior
Signed-off-by: jrabbit <jackjrabbit@gmail.com>
2016-02-23 11:52:51 -08:00
jrabbit
34d8f9b55a Mangle the tests. They pass for better or worse!
Signed-off-by: jrabbit <jackjrabbit@gmail.com>
2016-02-23 11:52:51 -08:00
jrabbit
f5533c1ed8 If an env var is passthrough but not defined on the host don't set it. This doesn't change too much code and keeps the generators.
Signed-off-by: jrabbit <jackjrabbit@gmail.com>
2016-02-23 11:52:51 -08:00
Daniel Nephin
722e3a2fc7 Merge remote-tracking branch 'docker/release' into bump-1.6.1 2016-02-23 11:15:34 -08:00
Aanand Prasad
9503aa2b5f Merge pull request #2821 from aanand/bump-1.6.0
Bump 1.6.0
2016-02-04 20:20:58 +00:00
Aanand Prasad
d99cad60e7 Bump 1.6.0
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-02-04 19:43:26 +00:00
Ben Firshman
8199c4a6e1 Improve names in Compose file 2 example
Just makes it a bit clearer what's going on.

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2016-02-04 19:43:26 +00:00
Aanand Prasad
19ae76a442 Update docker-py and dockerpty
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-02-04 19:41:11 +00:00
Daniel Nephin
a55210413c Update docs for version being a string.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-04 17:21:15 +00:00
Spencer Rinehart
6caa188730 Fix example formatting for depends_on.
Markdown was acting against expectations here by merging the example indented YAML into the previous list item instead of treating it as a code block.

I decided that a better way of handling this would be to add a "Simple example:" line that is also used elsewhere in this file.  It resets the markdown indentation in a way that works.

Signed-off-by: Spencer Rinehart <anubis@overthemonkey.com>
2016-02-04 15:57:29 +00:00
Aanand Prasad
413a55aa71 Connect container to networks before starting it
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-02-04 14:43:11 +00:00
Dimitar Bonev
64336615cf Falling back to default project name when COMPOSE_PROJECT_NAME is set to empty
Signed-off-by: Dimitar Bonev <dimitar.bonev@gmail.com>
2016-02-03 18:21:44 +00:00
Mary Anthony
a59982eb11 Fixing duplicate identifiers
Signed-off-by: Mary Anthony <mary@docker.com>
2016-02-03 18:21:44 +00:00
Daniel Nephin
46f33f12b0 Update merge docs with depends_on, and correction about how links and volumes_from are merged.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-03 18:21:44 +00:00
Daniel Nephin
dc718eae65 Make links unique-by-alias when merging
Factor out MergeDict from merge_service_dicts to reduce complexity below limit.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-03 18:21:44 +00:00
Daniel Nephin
aa5ff05463 Fix merging of lists with multiple files.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-03 18:21:44 +00:00
Ryan Taylor Long
3b7471ae84 Add depends_on to ALLOWED_KEYS
This ensures and already existing `depends_on` is not deleted when the service on which it is defined also employs `extends`.

Signed-off-by: Ryan Taylor Long <ryan@rtlong.com>
2016-02-03 18:21:44 +00:00
Daniel Nephin
7f009aeeb9 Move command reference to overview.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-03 18:21:44 +00:00
Daniel Nephin
44c7d080bd Rename the old environment variable page to link environment variables.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-03 18:21:43 +00:00
Daniel Nephin
32bd760526 Cleanup command-line reference docs by renaming overview to envvars.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-03 18:21:43 +00:00
Daniel Nephin
009dbbe971 Use the same capitalization for all menu items in the docs.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-03 18:21:43 +00:00
Daniel Nephin
6b59ba0c31 Re-order compose docs so that quickstart guides come before other documentation.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-03 18:21:43 +00:00
Daniel Nephin
61d00ebee4 Extract volume init and removal from project.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-03 18:21:43 +00:00
Daniel Nephin
3b1276bd44 Include networks in the config_hash.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-03 18:21:43 +00:00
Daniel Nephin
3e8a4a5dc3 Don't initialize networks that aren't used by any services.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-03 18:21:43 +00:00
Daniel Nephin
0c87e0b18f Refactor project network initlization.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-03 18:21:43 +00:00
Aanand Prasad
8024f2f09e Tweak and test warning shown when version is a dict
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-02-03 18:21:43 +00:00
Aanand Prasad
f081376067 Improve error messages for invalid versions
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-02-03 18:21:42 +00:00
Aanand Prasad
ce0d469c18 Make 'version' a string
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-02-03 18:21:42 +00:00
Aanand Prasad
b2ee08f439 Remove redundant check - self.config should never be None
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-02-03 18:21:42 +00:00
Aanand Prasad
e9ba06ed4b Normalise/fix config field designators in validation messages
- Instead of "Service 'web' configuration key 'image'", just say
  "web.image"

- Fix the "Service 'services'" bug in the v2 file format

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-02-03 18:21:42 +00:00
Aanand Prasad
9e9b36460c Convert validation error tests to pytest style
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-02-03 18:21:42 +00:00
Harald Albers
25df0d8147 bash completion for docker-compose create
Signed-off-by: Harald Albers <github@albersweb.de>
2016-02-01 15:53:22 +00:00
Steve Durrheimer
c98c617c30 Add zsh completion for 'docker-compose create'
Signed-off-by: Steve Durrheimer <s.durrheimer@gmail.com>
2016-02-01 15:53:22 +00:00
Aanand Prasad
d3cd9213c1 Fix rebase-bump-commit script
Trim whitespace from wc's output before constructing arguments to `git rebase`

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-02-01 15:51:16 +00:00
Daniel Nephin
bbaae11a0f Fix race condition with up and setting signal handlers.
Also print stdout on wait_for_container().

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-01 15:51:16 +00:00
Harald Albers
e925b8272b Fix computation of service list in bash completion
The previous approach assumed that the service list could be extracted
from a single file. It did not follow extends and overrides.

Signed-off-by: Harald Albers <github@albersweb.de>
2016-02-01 15:51:16 +00:00
Aanand Prasad
110401b6f0 Let the user specify any repo as their fork
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-02-01 15:51:16 +00:00
Aanand Prasad
8fb90bd732 Default to vim if EDITOR is not set
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-02-01 15:51:15 +00:00
Daniel Nephin
24e71db345 Don't copy over volumes that were previously host volumes, and are now container volumes.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-02-01 15:51:15 +00:00
Mary Anthony
fbe8484377 New navigation for 1.10 release
Updating with Joffrey's comments

Signed-off-by: Mary Anthony <mary@docker.com>
2016-02-01 15:51:15 +00:00
Aanand Prasad
89cca7bcb2 Extract helper methods
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-02-01 15:51:15 +00:00
Aanand Prasad
be66779fe9 Create declarations for named volumes
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-02-01 15:51:15 +00:00
Aanand Prasad
1772909fe2 Make sure version line is at the top of the file
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-02-01 15:51:15 +00:00
Aanand Prasad
86bdab64ab Make warnings a bit more readable
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-02-01 15:51:15 +00:00
Aanand Prasad
36a10f8dd5 Update for links, external_links, network_mode
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-02-01 15:51:15 +00:00
Aanand Prasad
9249ec62c2 Add note about named volumes to upgrade guide
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-02-01 15:51:15 +00:00
Aanand Prasad
4537ec70cc Remove outdated warnings about links from docs
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-01-27 13:33:38 +00:00
Aanand Prasad
695c692be6 Bump 1.6.0-rc2
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-01-27 01:42:21 +00:00
Aanand Prasad
297d20f085 Remove ability to join bridge network + user-defined networks
Containers connected to the bridge network can't have aliases, so it's
simpler to rule that they can *either* be connected to the bridge
network (via `network_mode: bridge`) *or* be connected to user-defined
networks (via `networks` or the default network).

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-01-27 01:42:21 +00:00
Joffrey F
16ef3d0eb8 Bump docker-py version to latest RC
Signed-off-by: Joffrey F <joffrey@docker.com>
2016-01-27 01:42:21 +00:00
Aanand Prasad
d765a3fb91 Add back external links in v2
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-01-27 00:45:44 +00:00
Aanand Prasad
7f4a94514b Fix trailing whitespace in docker-compose.md
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-01-26 23:41:47 +00:00
Tobias Munk
c0fe545947 fixed documentation about traversing yml files
Signed-off-by: Tobias Munk <schmunk@usrbin.de>
2016-01-26 18:58:15 +00:00
Aanand Prasad
52e74ab7ad Rename 'net' to 'network mode' in various classes/methods
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-01-26 11:52:23 +00:00
Aanand Prasad
2b46685855 Test that net can be extended
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-01-26 11:52:23 +00:00
Aanand Prasad
2b7306967b Implement network_mode in v2
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-01-26 11:52:23 +00:00
Joffrey F
3f28472ebc Move named volumes matching to config validation phase
Signed-off-by: Joffrey F <joffrey@docker.com>
2016-01-26 11:52:23 +00:00
Joffrey F
3da25aa463 is_named_volume also tests for home paths ~
Fix bug with VolumeSpec not being updated
Fix integration test

Signed-off-by: Joffrey F <joffrey@docker.com>
2016-01-26 11:52:23 +00:00
Joffrey F
a66bf72199 Match named volumes in service definitions with declared volumes
Raise ConfigurationError for undeclared named volumes
Test new behavior

Signed-off-by: Joffrey F <joffrey@docker.com>
2016-01-26 11:52:23 +00:00
Aanand Prasad
883227c4d8 Alias containers by short id
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-01-26 11:52:23 +00:00
Dimitar Bonev
5545c55ecc Network fields schema validation
Signed-off-by: Dimitar Bonev <dimitar.bonev@gmail.com>
2016-01-26 11:52:22 +00:00
Joffrey F
227fa5c0de Use latest docker-py rc
Signed-off-by: Joffrey F <joffrey@docker.com>
2016-01-26 11:52:22 +00:00
Aanand Prasad
d399b7893f Add test for logs=False
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>

Conflicts:
	compose/cli/main.py
2016-01-26 11:52:22 +00:00
Alf Lervag
833e16117e Fixes #2448
Signed-off-by: Alf Lervag <alf.lervag@bouvet.no>

Conflicts:
	compose/cli/main.py
	requirements.txt
2016-01-26 11:52:22 +00:00
Daniel Nephin
bf068a8287 Add stop signal to the docs.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-01-26 11:52:22 +00:00
Aanand Prasad
b1ebf5ce17 Fix interactive run with networking
Make sure we connect the container to all required networks *after*
starting the container and *before* hijacking the terminal.

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-01-26 11:52:22 +00:00
Aanand Prasad
836ec70979 Stop connecting to all networks on container creation
This relies on an Engine behaviour which is a bug, not an intentional
feature - we have to connect to networks one at a time

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-01-26 11:52:22 +00:00
Aanand Prasad
6ca410fd6b Fix 'run' behaviour with networks
- Test that one-off containers join all networks
- Don't set any aliases

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-01-26 11:52:22 +00:00
Joffrey F
38a6d04852 Update documentation for external param
Signed-off-by: Joffrey F <joffrey@docker.com>

Conflicts:
	docs/compose-file.md
2016-01-26 11:52:22 +00:00
Aanand Prasad
0952c1bb51 Add links to networks key references
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-01-26 11:52:21 +00:00
Aanand Prasad
18a1829db0 Update documentation for links
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-01-26 11:52:21 +00:00
Aanand Prasad
e40de088f3 Document depends_on
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-01-26 11:52:21 +00:00
Aanand Prasad
6fe54f5c24 Update Compose file documentation for version 2
- Explain each version in its own section
- Explain how to upgrade from version 1 to 2
- Note which keys are restricted to particular versions
- A few corrections to the docs for version-specific keys

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-01-26 11:52:21 +00:00
Aanand Prasad
5cfd947f38 Stop and remove containers in parallel when scaling down
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-01-26 11:52:21 +00:00
Aanand Prasad
513a6b35cc Fix scale when containers exit immediately
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-01-26 11:52:21 +00:00
Aanand Prasad
cbec6f8834 Support links in v2 files
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-01-26 11:52:21 +00:00
Aanand Prasad
76bc06b729 Fix Windows build failures when installing dependencies from git
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-01-26 11:52:21 +00:00
Jure Žvelc
c9ef1fa32f Fix for extending services written in v2 format.
Signed-off-by: Jure Žvelc <jzvelc@gmail.com>
2016-01-26 11:52:20 +00:00
Daniel Nephin
c52eed66b7 Update tests in sort_services_test.py to use pytest.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-01-26 11:52:20 +00:00
Daniel Nephin
3f65bdcf46 Move ulimits validation to validation.py and improve the error message.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-01-26 11:52:20 +00:00
Daniel Nephin
6e5c312768 Implement depends_on to define an order for services in the v2 format.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-01-26 11:52:20 +00:00
Daniel Nephin
33bb7c4e02 Add migration script.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-01-26 11:52:20 +00:00
Aanand Prasad
bb377d3fe6 Fix "name is reserved" with Engine 1.10 RC1
Ensure link aliases are unique (this deduping was previously performed
on the server).

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-01-26 11:52:20 +00:00
Aanand Prasad
7442b416e8 Test that you can set the default network to be external
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-01-26 11:52:20 +00:00
Aanand Prasad
fbee4ce4b3 Catch TLSParameterErrors from docker-py
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-01-26 11:52:20 +00:00
Steve Durrheimer
8398382b65 Add zsh completion for 'docker-compose up --abort-on-container-exit'
Signed-off-by: Steve Durrheimer <s.durrheimer@gmail.com>
2016-01-26 11:52:19 +00:00
Steve Durrheimer
7dd5fd5763 Add zsh completion for 'docker-compose down'
Signed-off-by: Steve Durrheimer <s.durrheimer@gmail.com>
2016-01-26 11:52:19 +00:00
Steve Durrheimer
d5f3826ec7 Fix zsh completion to ensure we have enough commands to store in the cache
Signed-off-by: Steve Durrheimer <s.durrheimer@gmail.com>
2016-01-26 11:52:19 +00:00
Daniel Nephin
20c936a251 Fix some bugs in release scripts.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-01-26 11:52:19 +00:00
Daniel Nephin
d2556a1347 Bump 1.6.0-rc1
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-01-18 14:50:56 -05:00
Aanand Prasad
237f134a00 Allow custom ipam config
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-01-18 14:50:56 -05:00
Aanand Prasad
d4720f85ef Update networking docs
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-01-18 14:03:52 -05:00
Aanand Prasad
64fc2b85cb Allow overriding of config for the default network
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-01-18 12:14:12 -05:00
Aanand Prasad
a22d248390 Quote network names in error messages
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2016-01-18 12:14:12 -05:00
Daniel Nephin
fffedfc87b Test against 1.10rc1.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-01-16 11:35:04 -05:00
Daniel Nephin
34fd042dbf Increase the timeout for all acceptance tests.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-01-15 18:12:57 -05:00
Daniel Nephin
89e31f7a8d Validate that an extended config file has the same version as the base.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2016-01-15 18:12:56 -05:00
Daniel Nephin
b446c09735 Merge remote-tracking branch 'docker/release' into bump-1.6.0-rc1 2016-01-15 17:02:06 -05:00
Daniel Nephin
8f48fa4747 Merge pull request #2491 from dnephin/bump-1.5.2
WIP: Bump 1.5.2
2015-12-03 17:11:51 -08:00
Daniel Nephin
7240ff35ee Bump 1.5.2
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-12-03 11:18:32 -08:00
Daniel Nephin
aaf66e3485 FAQ document for Compose
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-12-03 11:05:06 -08:00
Aanand Prasad
96f4a42a35 Validate the 'expose' option
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-12-03 11:05:05 -08:00
Aanand Prasad
e6fbca42a1 Split out ports validation tests into type, uniqueness, format
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-12-03 11:05:05 -08:00
Aanand Prasad
527bf3b023 Fix ports validation message
- The `raises` kwarg to the `cls_check` decorator was being used
  incorrectly (it should be an exception class, not an object).

- We need to check for `error.cause` and get the message out of the
  exception object.

NB: The particular case where validation fails in the case of `ports` is
only when ranges don't match in length - no further validation is
currently performed client-side.

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-12-03 11:05:05 -08:00
Aanand Prasad
ab36c9c6cd Refactor ports section of fields schema
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-12-03 11:05:05 -08:00
Aanand Prasad
e67419065a Fix ports validation test
We were essentially only testing that *at least one* of the invalid
values fails the validation check, rather than that *all* of them fail.

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-12-03 11:05:05 -08:00
Daniel Nephin
69e956ce8b Add integration test and docs for build with a git url.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-12-03 11:05:05 -08:00
Jonas Eckerström
0dbd99bad2 Added support for url buid paths
Signed-off-by: Jonas Eckerström <jonaseck@gmail.com>
2015-12-03 11:05:05 -08:00
Daniel Nephin
fa975d7fbe Properly resolve environment from all sources.
Split env resolving into two phases. The first phase is to expand the paths
of env_files, which is done before merging extends. Once all files are merged
together, the final phase is to read the env_files and use them as the base
for environment variables.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-12-03 11:05:01 -08:00
Daniel Nephin
81f0e72bd2 Move service sorting to config package.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-12-02 17:31:48 -08:00
Daniel Nephin
da27f8e7e2 Remove unnecessary intermediate variables in get_container_host_config.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-12-02 17:31:48 -08:00
Daniel Nephin
8572d50903 Move volume parsing to config.types module
This removes the last of the old service.ConfigError

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-12-02 17:31:48 -08:00
Daniel Nephin
5d39813e1b Fixes #2008 - re-use list_or_dict schema for all the types
At the same time, moves extra_hosts validation to the config module.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-12-02 17:31:48 -08:00
Daniel Nephin
b19315b57e Move restart spec to the config.types module.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-12-02 17:31:48 -08:00
Daniel Nephin
e549875e89 Move parsing of volumes_from to the last step of config parsing.
Includes creating a new compose.config.types module for all the domain objects.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-12-02 17:31:48 -08:00
Daniel Nephin
7e21b05f05 Remove project name validation
project name is already normalized to a valid name before creating a service.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-12-02 17:31:48 -08:00
Daniel Nephin
bea2072b95 Add the git sha to version output
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-12-02 17:31:48 -08:00
Daniel Nephin
3b6cc7a7bb Add missing assert and autospec.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-12-02 17:31:48 -08:00
Daniel Nephin
a264470cc0 Make sure we always have the latest busybox image, so that build --pull tests don't flake.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-12-02 17:31:48 -08:00
Daniel Nephin
844e2c3d26 Fix use case link in readme.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-12-02 17:31:48 -08:00
Daniel Nephin
210a14cf28 Add note about required pip version.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-12-02 17:31:48 -08:00
Brandon Burton
9ce4024951 Fixing matrix include so os: linux goes to trusty
Signed-off-by: Brandon Burton <brandon@inatree.org>
2015-12-02 17:31:48 -08:00
Daniel Nephin
8fb6fb7b19 Fix env_file and environment when used with extends.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-12-02 17:31:48 -08:00
Daniel Nephin
83760d0e9e Handle both SIGINT and SIGTERM for docker-compose run.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-12-02 17:31:48 -08:00
Daniel Nephin
be5b7b6f0e Handle both SIGINT and SIGTERM for docker-compose up.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-12-02 17:31:48 -08:00
Daniel Nephin
e5a02d3052 Fix extra warnings on masked volumes.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-12-02 17:31:48 -08:00
Stéphane Seguin
3a395892fc Fix restart with stopped containers. Fixes #1814
Signed-off-by: Stéphane Seguin <stephseguin93@gmail.com>
2015-12-02 17:31:48 -08:00
Daniel Nephin
09f6a876cf Fixes #2398 - the build progress stream can contain empty json objects.
Previously these empty objects would hit a bug in splitting objects causing it crash.
With this fix the empty objects are returned properly.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-12-02 17:31:48 -08:00
Stefan Scherer
0117148a36 Use uname to build target name for different platforms
Signed-off-by: Stefan Scherer <scherer_stefan@icloud.com>
2015-12-02 17:31:48 -08:00
Simon van der Veldt
8f70c8cdeb run.sh script: Also pass DOCKER_TLS_VERIFY and DOCKER_CERT_PATH env vars to compose container
Signed-off-by: Simon van der Veldt <simon.vanderveldt@gmail.com>
2015-12-02 17:31:48 -08:00
Daniel Nephin
16a74f3797 Fix texttable dep. 0.8.2 was removed from pypi.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-12-02 17:31:48 -08:00
Viranch Mehta
c42918ec7c Fix specifies_host_port() to handle port binding with host IP but no host port
Signed-off-by: Viranch Mehta <viranch.mehta@gmail.com>
2015-12-02 17:31:48 -08:00
Mazz Mosley
d28b2027b8 Clarify dockerfile requires build key
Credit to @funkyfuture for the first PR addressing the clarification.
https://github.com/docker/compose/pull/1767

Signed-off-by: Mazz Mosley <mazz@houseofmnowster.com>
2015-12-02 17:31:48 -08:00
Mazz Mosley
8d816fc2f3 Add cross references for env/cli
Signed-off-by: Mazz Mosley <mazz@houseofmnowster.com>
2015-12-02 17:31:48 -08:00
Daniel Nephin
f476436027 Merge remote-tracking branch 'docker/release' into bump-1.5.2 2015-12-02 16:56:55 -08:00
Daniel Nephin
fae20305ec Merge pull request #2384 from dnephin/bump-1.5.1
**WIP** Bump 1.5.1
2015-11-12 17:29:43 -05:00
Daniel Nephin
4628e93fb2 Bump 1.5.1
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 15:02:12 -05:00
Daniel Nephin
82086a4e92 Remove name field from the list of ALLOWED_KEYS
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 15:02:12 -05:00
Daniel Nephin
96e9b47059 Inclide the filename in validation errors.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 15:02:12 -05:00
Daniel Nephin
34166ef5a4 Refactor process_errors into smaller functions
So that it passed new max-complexity requirement

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 15:02:12 -05:00
Daniel Nephin
285e52cc7c Add ids to config schemas
Also enforce a max complexity for functions and add some new tests for config.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 15:02:12 -05:00
Joffrey F
d52c969f94 Add test for environment variable dashes support
Signed-off-by: Joffrey F <joffrey@docker.com>
2015-11-12 13:54:41 -05:00
Joffrey F
63c3e6f58c Allow dashes in environment variable names
See http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html
Environment variable names used by the utilities in the Shell and
Utilities volume of POSIX.1-2008 consist solely of uppercase letters,
digits, and the <underscore> ( '_' ) from the characters defined in
Portable Character Set and do not begin with a digit. Other characters may
be permitted by an implementation; applications shall tolerate the
presence of such names.

Signed-off-by: Joffrey F <joffrey@docker.com>
2015-11-12 13:54:41 -05:00
Daniel Nephin
0ab76bb8bc Add a test for invalid field 'name', and fix an existing test for invalid service names.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 13:54:41 -05:00
Daniel Nephin
7fc577c31d Remove name from config schema.
Refactors config validation of a service to use a ServiceConfig data object.
Instead of passing around a bunch of related scalars, we can use the
ServiceConfig object as a parameter to most of the service validation functions.

This allows for a fix to the config schema, where the name is a field in the
schema, but not actually in the configuration. My passing the name around as
part of the ServiceConfig object, we don't need to add it to the config options.
Fixes #2299

validate_against_service_schema() is moved from a conditional branch in
ServiceExtendsResolver() to happen as one of the last steps after all
configuration is merged. This schema only contains constraints which only need
to be true at the very end of merging.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 13:54:41 -05:00
Daniel Nephin
3a43110f06 Fix a bug in ExtendsResolver where the service name of the extended service was wrong.
This bug can be seen by the change to the test case. When the extended service
uses a different name, the error was reported incorrectly.

By fixing this bug we can simplify self.signature and self.detect_cycles to
always use self.service_name.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 13:54:41 -05:00
Daniel Nephin
87d79d4d99 Rename ServiceLoader to ServiceExtendsResolver
ServiceLoader has evolved to be not really all that related to "loading" a
service. It's responsibility is more to do with handling the `extends`
field, which is only part of loading.  The class and its primary method
(make_service_dict()) were renamed to better reflect their responsibility.

As part of that change process_container_options() was removed from
make_service_dict() and renamed to process_service().  It contains logic for
handling the non-extends options.

This change allows us to remove the hacks from testcase.py and only call
the functions we need to format a service dict correctly for integration tests.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 13:54:41 -05:00
Daniel Nephin
83581c3a0f Validate additional files before merging them.
Consolidates all the top level config handling into `process_config_file` which
is now used for both files and merge sources.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 13:54:41 -05:00
Joffrey F
ba90f55075 Reorganize conditional branches to improve readability
Signed-off-by: Joffrey F <joffrey@docker.com>
2015-11-12 13:54:41 -05:00
Yves Peter
3313dcb1ce Fixes #1490 progress_stream would print a lot of new lines on "docker-compose pull" if there's no tty.
Signed-off-by: Yves Peter <ypdraw@gmail.com>
2015-11-12 13:54:41 -05:00
Daniel Nephin
92d56fab47 Add a warning when the host volume config is being ignored.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 13:54:41 -05:00
Daniel Nephin
1208f92d9c Update doc wording for ulimits.
and move tests to the correct module

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 13:54:41 -05:00
Kevin Greene
8444551373 Added ulimits functionality to docker compose
Signed-off-by: Kevin Greene <kevin@spantree.net>
2015-11-12 13:54:41 -05:00
Daniel Nephin
73ebd7e560 Only create the default network if at least one service needs it.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 13:54:41 -05:00
Daniel Nephin
0a96f86f74 Cleanup workaround in testcase.py
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 13:54:41 -05:00
Daniel Nephin
36176befb0 Fix #1549 - flush after each line of logs.
Includes some refactoring of log_printer_test to support checking for flush(), and so that each test calls the unit-under-test directly, instead of through a helper function.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 13:54:41 -05:00
Daniel Nephin
de08da278d Re-order flags in bash completion
and remove unnecessary variables from build command.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 13:54:41 -05:00
Adrian Budau
4c2eb17ccd Added --force-rm to compose build.
It's a flag passed to docker build that removes the intermediate
containers left behind on fail builds.

Signed-off-by: Adrian Budau <budau.adi@gmail.com>
2015-11-12 13:54:41 -05:00
Daniel Nephin
8fb44db92b Cleanup some unit tests and whitespace.
Remove some unnecessary newlines.
Remove a unittest that was attempting to test behaviour that was removed a while ago, so isn't testing anything.
Updated some unit tests to use mocks instead of a custom fake.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 13:54:41 -05:00
Daniel Nephin
4105c3017c Move cli tests to a new testing package.
These cli tests are now a different kind of that that run the compose binary. They are not the same as integration tests that test some internal interface.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 13:54:40 -05:00
Daniel Nephin
7f2f4eef48 Update cli tests to use subprocess.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 13:54:40 -05:00
Joffrey F
666c3cb1c7 Use exit code 1 when encountering a ReadTimeout
Signed-off-by: Joffrey F <joffrey@docker.com>
2015-11-12 13:54:40 -05:00
Daniel Nephin
886134c1f3 Recreate dependents when a dependency is created (not just when it's recreated).
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 13:54:40 -05:00
Daniel Nephin
ba61a6c5fb Don't set the hostname to the service name with networking.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 13:54:40 -05:00
Daniel Nephin
3f14df374f Handle non-utf8 unicode without raising an error.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 13:54:40 -05:00
Daniel Nephin
e6755d1e7c Use VolumeSpec instead of re-parsing the volume string.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 13:54:40 -05:00
Daniel Nephin
c4f59e731d Make working_dir consistent in the config package.
- make it a positional arg, since it's required
- make it the first argument for all functions that require it
- remove an unnecessary one-line function that was only called in one place

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 13:54:39 -05:00
Daniel Nephin
805ed344c0 Refactor ServiceLoader to be immutable.
Mutable objects are harder to debug and harder to reason about. ServiceLoader was almost immutable. There was just a single function which set fields for a second function. Instead of mutating the object, we can pass those values as parameters to the next function.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 13:54:39 -05:00
Daniel Nephin
a5959d9be2 Some minor style cleanup
- fixed a docstring to make it PEP257 compliant
- wrapped some long lines
- used a more specific error

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 13:54:39 -05:00
Mazz Mosley
0375dccf64 Handle non-ascii chars in volume directories
Signed-off-by: Mazz Mosley <mazz@houseofmnowster.com>
2015-11-12 13:54:39 -05:00
Daniel Nephin
3c4bb5358e Upgrade pyyaml to 3.11
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 13:54:39 -05:00
Daniel Nephin
e317d2db9d Remove service.start_container()
It has been an unnecessary wrapper around container.start() for a little while now, so we can call it directly.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 13:54:39 -05:00
Joffrey F
3daecfa8e4 Update service config_dict computation to include volumes_from mode
Ensure config_hash is updated when volumes_from mode is changed, and
service is recreated on next up as a result.

Signed-off-by: Joffrey F <joffrey@docker.com>
2015-11-12 13:54:39 -05:00
Aanand Prasad
cf93362368 Fix parallel output
We were outputting an extra line, which in *some* cases, on *some*
terminals, was causing the output of parallel actions to get messed up.

In particular, it would happen when the terminal had just been cleared
or hadn't yet filled up with a screen's worth of text.

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-11-12 13:54:39 -05:00
Daniel Nephin
23d4eda2a5 Fix service recreate when image changes to build.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 13:54:39 -05:00
Daniel Nephin
718ae13ae1 Move config hash tests to service_test.py
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-12 13:54:39 -05:00
Daniel Nephin
cddbe9fbf1 Merge remote-tracking branch 'docker/release' into bump-1.5.1 2015-11-12 13:03:47 -05:00
Aanand Prasad
9c8173dbfd Merge pull request #2309 from dnephin/bump-1.5.0
WIP: Bump 1.5.0
2015-11-03 18:31:56 +00:00
Daniel Nephin
77ff37a853 Bump 1.5.0
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-03 12:36:30 -05:00
Daniel Nephin
40341674bd Re-order extends docs.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-03 12:36:30 -05:00
Daniel Nephin
62ebdce5a9 Replace composition with Compose app.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-03 12:36:30 -05:00
Daniel Nephin
58de4e0c26 Document using multiple Compose files use cases.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-03 12:36:30 -05:00
Daniel Nephin
dbd6c62b70 Changes to production.md for working with multiple Compose files.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-03 12:36:30 -05:00
Daniel Nephin
887c6753f8 Support a volume to the docs directory and add --watch, so docs can be refreshed.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-03 12:36:30 -05:00
Daniel Nephin
bd35896892 Remove duplication from extends docs.
Start restructuring extends docs in preparation for adding documentation about using multiple compose files.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-03 12:36:30 -05:00
Daniel Nephin
9286e62449 On a test failure only show the last 100 lines of daemon output.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-03 11:54:59 -05:00
Daniel Nephin
621d1a5167 Fix networking tests to work with new API in engine rc4 (https://github.com/docker/docker/pull/17536)
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-03 11:54:59 -05:00
Daniel Nephin
83714fbac2 Touch up intro paragraph with feedback from @moxiegirl.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-03 11:33:23 -05:00
Daniel Nephin
413921a287 Add another feature to the docs - multiple environments per host.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-03 11:33:23 -05:00
Daniel Nephin
7ee36829ac Update intro docs based on feedback.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-03 11:33:23 -05:00
Daniel Nephin
bfb46b37d3 Updates to gettingstarted guide from PR feedback.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-03 11:33:23 -05:00
Daniel Nephin
09d2bdbb21 Flush out features and use cases.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-03 11:33:23 -05:00
Daniel Nephin
8733d09a9c Extract the getting started guide from the index page.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-03 11:33:23 -05:00
Daniel Nephin
e524cce222 Add missing title to compose file reference.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-11-03 11:33:23 -05:00
Daniel Nephin
be6b811c4e Bump 1.5.0rc3
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-10-30 15:53:17 -04:00
Daniel Nephin
bdb9a280bc Make storage driver configurable in CI
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-10-30 15:53:16 -04:00
Daniel Nephin
73ca4eb599 On error print daemon logs
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-10-30 15:46:57 -04:00
Daniel Nephin
569ccbadec Convert the README to rst and fix the logo url before packaging it up for pypi.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-10-30 15:46:57 -04:00
Daniel Nephin
ed1b584c42 Fix release script notes about software and typos.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-10-30 15:46:57 -04:00
Daniel Nephin
2f2e946907 Don't set a default network driver, let the server decide.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-10-30 15:36:15 -04:00
Daniel Nephin
d392f70cc6 Fixes #1843, #1936 - chown files back to host user in django example.
Also add a missing 'touch Gemfile.lock' to fix the rails tutorial.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-10-30 15:36:15 -04:00
Daniel Nephin
db164cefd3 Remove the duplicate 'Warning' prefix now that the logger adds the prefix.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-10-30 15:36:15 -04:00
Daniel Nephin
4d613d3ba7 Use colors when logging warnings or errors, so they are more obvious.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-10-30 15:36:15 -04:00
Joffrey F
1f26841e23 Integration test for run command with networking enabled
Signed-off-by: Joffrey F <joffrey@docker.com>
2015-10-30 15:36:15 -04:00
Joffrey F
9370cb033c Ensure network exists when calling run before up
Otherwise the daemon will error out because the network doesn't exist
yet.

Signed-off-by: Joffrey F <joffrey@docker.com>
2015-10-30 15:36:15 -04:00
Mazz Mosley
ab0ddb593f Clarify the command is an example
Signed-off-by: Mazz Mosley <mazz@houseofmnowster.com>
2015-10-29 15:01:33 -04:00
Daniel Nephin
f67503d9fd Logs are available for all log drivers except for none.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-10-29 15:01:33 -04:00
Daniel Nephin
ce729b0721 Update docs about networking for current release.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-10-29 15:01:33 -04:00
Daniel Nephin
8156cdc56e Disable a test against docker 1.8.3 because it fails due to a bug in docker engine.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-10-29 15:01:33 -04:00
Daniel Nephin
8cc8e61474 Bump 1.5.0rc2
Signed-off-by: Daniel Nephin <dnephin@docker.com>

Fill out 1.5.0 release notes

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-10-27 19:48:54 -04:00
Sven Dowideit
29b0ffe5e9 Possible link fixes
Signed-off-by: Sven Dowideit <SvenDowideit@home.org.au>
2015-10-27 19:33:28 -04:00
Mazz Mosley
a772a0d7d7 Remove redundant try/except
Code cleanup. We no longer need this as the api returns a 304 for any
stopped containers, which doesn't raise an error.

Signed-off-by: Mazz Mosley <mazz@houseofmnowster.com>
2015-10-27 19:33:28 -04:00
Mazz Mosley
6f0096c87b Move rename functionality into Container
Signed-off-by: Mazz Mosley <mazz@houseofmnowster.com>
2015-10-27 19:33:28 -04:00
Mazz Mosley
da41ed22f9 Fix tests
Signed-off-by: Mazz Mosley <mazz@houseofmnowster.com>
2015-10-27 19:33:28 -04:00
Mazz Mosley
d6fa8596d2 Attach to a container's log_stream before they're started
So we're not displaying output of all previous logs for a container, we attach,
if possible, to a container before the container is started.

LogPrinter checks if a container has a log_stream already attached and
print from that rather than always attempting to attach one itself.

Signed-off-by: Mazz Mosley <mazz@houseofmnowster.com>
2015-10-27 19:33:28 -04:00
Daniel Nephin
a9b4fe768d Fix running one-off containers with --x-networking by disabling linking to self.
docker create fails if networking and links are used together.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-10-27 19:33:28 -04:00
Daniel Nephin
88e53e177d Upgrade pyinstaller to 3.0
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-10-26 13:05:41 -04:00
Daniel Nephin
e168fd03ca Fix unicode in environment variables for python2.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-10-26 13:05:41 -04:00
Daniel Nephin
95a23eb682 Change version check from engine version to api version.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-10-26 13:05:40 -04:00
Daniel Nephin
f5ad363143 Use inspect network to query for an existing network.
And more tests for get_network()

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-10-26 13:05:40 -04:00
Daniel Nephin
f290faf4ba Minor refactor to use guard and replace instead of split+join
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-10-21 11:29:49 -04:00
Daniel Nephin
725088a18b Force windows drives to be lowercase.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-10-21 11:29:49 -04:00
Daniel Nephin
bf672ec340 Fixes #2205 - extends must be copied from override file.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-10-20 11:27:11 -04:00
Mazz Mosley
0e4f9c9a66 Environment keys can contain empty values
Environment keys that contain no value, get populated with values taken
from the environment not from the build phase but from running the command `up`.

Signed-off-by: Mazz Mosley <mazz@houseofmnowster.com>
2015-10-19 15:16:19 -04:00
Mazz Mosley
5fdb75b541 Improve error message for type constraints
It was missing a space between the different types, when there
were 3 possible type values.

Signed-off-by: Mazz Mosley <mazz@houseofmnowster.com>
2015-10-19 15:16:19 -04:00
Daniel Nephin
24d4a1045a Fixes #2203 - properly validate files when multiple files are used.
Remove the single-use decorators so the functionality can be used directly as a function.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-10-19 15:16:19 -04:00
Daniel Nephin
514f0650b2 Give the user a better error message (without a stack trace) when there is a yaml error.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-10-19 12:02:40 -04:00
Steve Durrheimer
20d34c8b14 Add zsh completion for 'docker-compose --x-networking --x-network-driver'
Signed-off-by: Steve Durrheimer <s.durrheimer@gmail.com>
2015-10-19 10:38:22 -04:00
Harald Albers
6f45eb7959 bash completion for networking options
Signed-off-by: Harald Albers <github@albersweb.de>
2015-10-19 10:11:35 -04:00
Mazz Mosley
49b98fa111 Attempt to document escaping env vars
People are likely to run into their env vars being set to empty strings,
if they're not aware that they need to escape them for Compose to not
interpolate them.

Signed-off-by: Mazz Mosley <mazz@houseofmnowster.com>
2015-10-16 15:52:32 -04:00
Cameron Eagans
6048630a11 docker-compose pull SERVICE should not pull SERVICE's dependencies
Signed-off-by: Cameron Eagans <me@cweagans.net>
2015-10-16 12:59:57 -04:00
Aanand Prasad
46de4411a7 Revert networking-related changes to getting started guides
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-10-16 12:59:57 -04:00
Karol Duleba
883f251e7d Docs for shorthand notation of extends. Issue #1989
Signed-off-by: Karol Duleba <mr.fuxi@gmail.com>
2015-10-16 12:59:57 -04:00
Daniel Nephin
284cda087e Add missing merge for release branch.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-10-16 12:59:57 -04:00
Daniel Nephin
b2f9c182f3 Fix some release docs.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-10-16 12:59:57 -04:00
Daniel Nephin
558098d322 Add a script to generate contributor list.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-10-16 12:59:57 -04:00
Per Persson
6571e079b9 Remove incorrectly placed comment
I'm not sure if it should be there at all, but at least it should hardly be where it currently is located.

Signed-off-by: Per Persson <per.persson@exertisztorm.com>
2015-10-16 12:59:57 -04:00
Tim Butler
49ca23c034 Fix link to Release Process doc in README.md
Signed-off-by: Tim Butler <tim.butler.au@gmail.com>
2015-10-16 12:59:57 -04:00
Daniel Nephin
709bd9c363 Bump 1.5.0rc1
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-10-14 12:59:31 -04:00
Daniel Nephin
8cecf2e02d Merge remote-tracking branch 'docker/release' into bump-1.5.0rc1 2015-10-14 12:59:25 -04:00
Daniel Nephin
d59c759cdd Merge pull request #2082 from dnephin/bump-1.4.2
[WIP] Release 1.4.2
2015-09-22 14:17:04 -04:00
Daniel Nephin
7b5d5fcd58 Bump 1.4.2
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-09-22 12:26:51 -04:00
Daniel Nephin
d01f712376 Fix a test case that assumes busybox image id.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-09-22 12:26:51 -04:00
Daniel Nephin
ac75d35927 Fix #1961 - docker-compose up should attach to all containers with no service names are specified, and add tests.
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
2015-09-22 11:09:59 -04:00
Daniel Nephin
605d7f26e7 Merge pull request #2027 from dnephin/bump-1.4.1
Bump 1.4.1
2015-09-15 17:47:09 -04:00
Daniel Nephin
b24ca75914 Bump 1.4.1
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-09-14 18:18:01 -04:00
Daniel Nephin
2b75741e5a Fix cherry-pick errors.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-09-11 14:22:35 -04:00
Daniel Nephin
7ff8c2b224 Resolves #1804
Fix mutation of service.options when a label or environment variable is specified in the config.

Signed-off-by: Daniel Nephin <dnephin@gmail.com>
2015-09-10 19:32:30 -04:00
Daniel Nephin
db31adc208 Extract link names into a function.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-09-10 19:32:30 -04:00
Daniel Nephin
805f6a7683 Refactor network_mode logic out of Service.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
2015-09-10 19:32:30 -04:00
Daniel Nephin
d92f323e6d Fixes #1757 - include all service properties in the config_dict()
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
2015-09-10 19:32:30 -04:00
Daniel Nephin
cf2dbf55b8 Cleanup some project logic.
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
2015-09-10 19:32:30 -04:00
Daniel Nephin
8d4c724c2d Sort config keys
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
2015-09-10 19:32:30 -04:00
Daniel Nephin
9cb2770da4 Make external_links a regular service.option so that it's part of the config hash
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
2015-09-10 19:32:30 -04:00
Daniel Nephin
6a23491fa9 Resolves #1856, fix regression in #1645. Includes some refactoring to make testing easier.
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
2015-09-10 19:32:30 -04:00
Mazz Mosley
294b9742be Handle all exceptions
If we get back an error that wasn't an APIError, it was causing the
thread to hang. This catch all, while I appreciate feels risky to
have a catch all, is better than not catching and silently failing,
with a never ending thread.

If something worse than an APIError has gone wrong, we want to stop
the incredible journey of what we're doing.

Signed-off-by: Mazz Mosley <mazz@houseofmnowster.com>
2015-09-10 19:32:30 -04:00
Aanand Prasad
a9b1f15f92 Fix volume path warning
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-09-10 19:32:29 -04:00
Aanand Prasad
aa7c7cdf93 Merge pull request #1763 from aanand/bump-1.4.0
Bump 1.4.0
2015-08-11 18:38:49 +01:00
Aanand Prasad
28139ab90d Bump 1.4.0
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-08-11 16:00:20 +01:00
Aanand Prasad
d0792b49fa Merge pull request #1846 from aanand/fix-mem-limit-options
Fix mem_limit and memswap_limit regression
(cherry picked from commit 93cc7e3751)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-08-11 16:00:20 +01:00
Mazz Mosley
5548aa5c79 Merge pull request #1833 from aanand/deprecate-relative-volumes-without-dot
Show a warning when a relative path is specified without "./"
(cherry picked from commit 52733f6996)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>

Conflicts:
	compose/config.py
	tests/unit/config_test.py
2015-08-11 12:57:42 +01:00
Ben Firshman
16440ff055 Merge pull request #1829 from vlajos/typofixes-vlajos-20150807
typofix - https://github.com/vlajos/misspell_fixer
(cherry picked from commit b7baa899e2)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-08-11 09:44:28 +01:00
Ben Firshman
7850d6de45 Merge pull request #1832 from aanand/use-docker-1.8.0-rc3
Test against Docker 1.8.0 RC3
(cherry picked from commit afc9629c59)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-08-11 09:44:28 +01:00
Ben Firshman
74b4fb89bb Merge pull request #1835 from aanand/fix-crash-when-container-has-no-name
Ignore containers that don't have a name
(cherry picked from commit 4e12ce39b3)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-08-11 09:44:27 +01:00
Ben Firshman
22ccf35fa1 Merge pull request #1836 from aanand/use-overlay-driver-in-tests
Use overlay driver in tests
(cherry picked from commit 197d332620)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-08-11 09:44:27 +01:00
Aanand Prasad
7ad1fe24bd Merge pull request #1815 from aanand/abort-if-daemon-cant-start
Abort tests if daemon fails to start
(cherry picked from commit f7b9daf927)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-08-11 09:44:27 +01:00
Aanand Prasad
450ba978c1 Merge pull request #1812 from moxiegirl/install-update-for-1811
Closes #1811 for Toolbox
(cherry picked from commit 6cb8e512f2)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>

Conflicts:
	docs/install.md
2015-08-10 12:51:10 +01:00
Aanand Prasad
3d6946417d Merge pull request #1800 from aanand/volume-driver-support
Support volume_driver
(cherry picked from commit 41b9df7639)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-07-31 16:29:24 +01:00
Daniel Nephin
31cf63b374 Merge pull request #1799 from d2bit/clean-rails-quickstart-guide-db-config
Remove useless postgres 'port' configuration
(cherry picked from commit b25f05bed4)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-07-31 15:46:07 +01:00
Daniel Nephin
5c853c4a2c Merge pull request #1794 from aanand/add-test-for-trailing-slash-volume-copy
Add test for trailing slash volume copying bug
(cherry picked from commit ea7276031c)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-07-31 15:29:44 +01:00
Aanand Prasad
ad922cd7a1 Merge pull request #1787 from aanand/fix-duplicate-volume-bind
Fix "Duplicate volume mount" error when config has trailing slashes
(cherry picked from commit dc7bdd10d4)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-07-31 15:29:44 +01:00
Aanand Prasad
49bafdc4cd Merge pull request #1777 from aanand/update-api-version
Update API version to 1.19
(cherry picked from commit 276e369c31)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-07-31 15:29:44 +01:00
Aanand Prasad
989b2491b9 Merge pull request #1780 from gheart/specify_api_version_via_env
Allow API version specification via env var
(cherry picked from commit 2759ab5ab6)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-07-31 15:29:44 +01:00
Aanand Prasad
ca2ce3a034 Merge pull request #1779 from aanand/mac-binary-error-hint
Add hint about OS X binary compatibility
(cherry picked from commit 1496734cbb)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-07-31 15:29:43 +01:00
Aanand Prasad
dfe9dccab8 Merge pull request #1774 from moxiegirl/test-entire-build
Contributors can build public docs with compose docs in context
(cherry picked from commit 487eae3b7b)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-07-31 15:29:43 +01:00
Aanand Prasad
d456c3909d Merge remote-tracking branch 'origin/release' into bump-1.4.0 2015-07-23 16:46:57 +01:00
Aanand Prasad
29ceef6d93 Merge pull request #1706 from aanand/bump-1.3.3
Bump 1.3.3
2015-07-16 11:40:20 +01:00
Aanand Prasad
8cff440800 Bump 1.3.3
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-07-16 11:21:01 +01:00
Mazz Mosley
e5f6ae767d Merge pull request #1704 from aanand/fix-timeout-type
Make sure up/restart/stop timeout is an int
(cherry picked from commit c7dccccd1f)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-07-16 11:19:21 +01:00
Aanand Prasad
cd44179305 Merge pull request #1705 from aanand/fix-labels-null
Handle case where /containers/json returns "Labels": null
(cherry picked from commit 7b9664be8e)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-07-15 17:33:08 +01:00
Aanand Prasad
c3c5b354b8 Merge pull request #1690 from aanand/bump-1.3.2
Bump 1.3.2
2015-07-14 18:04:22 +01:00
Aanand Prasad
95cf195dbd Bump 1.3.2
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-07-14 17:40:43 +01:00
Aanand Prasad
a80afd67ab Merge pull request #1688 from aanand/use-docker-py-1.3.0
Use docker-py 1.3.0
(cherry picked from commit 1e71eebc74)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-07-14 17:29:25 +01:00
Aanand Prasad
4bc4d273ac Merge pull request #1643 from aanand/warn-about-legacy-one-off-containers
Show an error on 'run' when there are legacy one-off containers
(cherry picked from commit 81707ef1ad)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-07-14 17:29:12 +01:00
Aanand Prasad
4911c77134 Merge pull request #1489 from dnephin/faster_integration_tests
Faster integration tests
(cherry picked from commit 5231288b4e)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>

Conflicts:
	compose/cli/main.py
2015-07-14 17:28:54 +01:00
Aanand Prasad
c1b9a76a54 Merge pull request #1658 from aanand/fix-smart-recreate-nonexistent-image
Fix smart recreate when 'image' is changed to something nonexistent
(cherry picked from commit 2bc10db545)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-07-14 17:25:33 +01:00
Aanand Prasad
c31e25af72 Merge pull request #1642 from aanand/fix-1573
Fix bug where duplicate container is leftover after 'up' fails
(cherry picked from commit f42fd6a3ad)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-07-14 17:25:03 +01:00
Aanand Prasad
c8295d36cc Merge pull request #1644 from aanand/fix-rm-bug
Stop 'rm' and 'ps' listing services not defined in the current file
(cherry picked from commit d85688892c)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-07-14 17:24:15 +01:00
Aanand Prasad
b12c29479e Merge pull request #1521 from dano/validate-service-names
Validate that service names passed to Project.containers aren't bogus.
(cherry picked from commit bc14c473c9)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-07-14 17:24:15 +01:00
Aanand Prasad
cd47829f3d Merge pull request #1588 from aanand/bump-1.3.1
Bump 1.3.1
2015-06-22 08:01:13 -07:00
Aanand Prasad
4d4ef4e0b3 Bump 1.3.1
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-21 17:32:36 -07:00
Aanand Prasad
882ef2ccd8 Merge pull request #1578 from aanand/fix-migrate-help
Fix 'docker-compose help migrate-to-labels'
(cherry picked from commit c8751980f9)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-21 17:28:18 -07:00
Aanand Prasad
d6cd76c3c1 Merge pull request #1570 from aanand/fix-build-pull
Explicitly set pull=False when building
(cherry picked from commit 4f83a18912)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-21 17:28:09 -07:00
Ben Firshman
bd0be2cdc7 Merge pull request #1580 from aanand/dont-set-network-mode-when-none-is-specified
Don't set network mode when none is specified
(cherry picked from commit 911cd60360)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-21 17:27:59 -07:00
Aanand Prasad
a8d7ebd987 Merge pull request #1461 from aanand/bump-1.3.0
Bump 1.3.0
2015-06-18 11:41:40 -07:00
Aanand Prasad
00f61196a4 Bump 1.3.0
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-18 11:25:10 -07:00
Aanand Prasad
c21d6706b6 Merge pull request #1565 from aanand/use-docker-1.7.0
Use docker 1.7.0 and docker-py 1.2.3
(cherry picked from commit 8ffeaf2a54)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>

Conflicts:
	Dockerfile
2015-06-18 11:25:10 -07:00
Aanand Prasad
c3c5d91c47 Merge pull request #1563 from moxiegirl/hugo-test-fixes
Hugo final 1.7 Documentation PR -- please read carefully
(cherry picked from commit 4e73e86d94)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-18 11:25:10 -07:00
Aanand Prasad
7fa4cd1214 Merge pull request #1552 from aanand/add-upgrade-instructions
Add upgrading instructions to install docs
(cherry picked from commit bc7161b475)
2015-06-16 16:29:18 -07:00
Aanand Prasad
f353d9fbc0 Merge pull request #1406 from vdemeester/667-compose-port-scale
Fixing docker-compose port with scale (#667)
(cherry picked from commit 5b2a0cc73d)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-15 11:22:08 -07:00
Daniel Nephin
09018855ce Merge pull request #1550 from aanand/update-docker-py
Update setup.py with new docker-py minimum
(cherry picked from commit b3b44b8e4c)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-15 11:22:08 -07:00
Aanand Prasad
719954b02f Merge pull request #1545 from moxiegirl/test-tooling
Updated for new documentation tooling
(cherry picked from commit aaccd12d3d)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-15 11:22:08 -07:00
Daniel Nephin
67bc3fabe4 Merge pull request #1544 from aanand/fix-volume-deduping
Fix volume binds de-duplication
(cherry picked from commit 77e594dc94)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-15 11:22:08 -07:00
Daniel Nephin
e724a346c7 Merge pull request #1526 from aanand/remove-start-or-create-containers
Remove Service.start_or_create_containers()
(cherry picked from commit 38a11c4c28)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-15 11:22:08 -07:00
Daniel Nephin
87b4545b44 Merge pull request #1508 from thaJeztah/update-dockerproject-links
Update dockerproject.com links
(cherry picked from commit 417e6ce0c9)
2015-06-15 11:22:07 -07:00
Aanand Prasad
58a7844129 Merge pull request #1482 from bfirsh/add-build-and-dist-to-dockerignore
Make it possible to run tests remotely
(cherry picked from commit c8e096e089)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-15 11:22:07 -07:00
Daniel Nephin
4353f7b9f9 Merge pull request #1475 from fordhurley/patch-1
Fix markdown formatting for `--service-ports` example
(cherry picked from commit d64bf88e26)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-15 11:22:07 -07:00
Aanand Prasad
8f8693e13e Merge pull request #1480 from bfirsh/change-sigint-test-to-use-sigstop
Change kill SIGINT test to use SIGSTOP
(cherry picked from commit a15f996744)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-15 11:22:07 -07:00
Ben Firshman
363a6563c7 Merge pull request #1537 from aanand/reorder-service-utils
Reorder service.py utility methods
(cherry picked from commit e3525d64b5)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-15 11:22:07 -07:00
Aanand Prasad
59d6af73fa Merge pull request #1539 from bfirsh/add-image-affinity-to-test
Add image affinity to test script
(cherry picked from commit 4c2112dbfd)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-15 11:22:07 -07:00
Aanand Prasad
cd7f67018e Merge pull request #1466 from noironetworks/changing-scale-to-warning
Modified scale awareness from exception to warning
(cherry picked from commit 7d2a89427c)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-15 11:22:07 -07:00
Ben Firshman
b7e8770c4f Merge pull request #1538 from thieman/tnt-serivce-misspelled
Correct misspelling of "Service" in an error message
(cherry picked from commit bd246fb011)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-15 11:22:07 -07:00
Aanand Prasad
ad4cc5d6df Merge pull request #1497 from aanand/use-1.7-rc1
Run tests against Docker 1.7 RC2
(cherry picked from commit 0e9ccd36f3)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-15 11:22:06 -07:00
Aanand Prasad
ca14ed68f7 Merge pull request #1533 from edmorley/update-b2d-shellinit-example
Docs: Update boot2docker shellinit example to use 'eval'
(cherry picked from commit 17e03b29f9)
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-15 11:22:06 -07:00
Daniel Nephin
71514cb380 Merge pull request #1531 from aanand/test-crash-resilience
Test that data volumes now survive a crash when recreating
(cherry picked from commit 87c30ae6e4)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-15 11:22:06 -07:00
Daniel Nephin
8212f1bd45 Merge pull request #1529 from aanand/update-dockerpty
Update dockerpty to 0.3.4
(cherry picked from commit 95b2eaac04)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-09 18:24:14 -04:00
Daniel Nephin
dca3bbdea3 Merge pull request #1527 from aanand/remove-logging-on-run-rm
Remove logging on run --rm
(cherry picked from commit 5578ccbb01)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-09 14:39:53 -04:00
Daniel Nephin
8ed7dfef6f Merge pull request #1525 from aanand/fix-duplicate-logging
Fix duplicate logging on up/run
(cherry picked from commit e2b790f732)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-09 14:39:52 -04:00
Daniel Nephin
631f5be02f Merge pull request #1481 from albers/completion-smart-recreate
Support --x-smart-recreate in bash completion
(cherry picked from commit 9a0bb325f2)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-09 14:39:52 -04:00
Ben Firshman
4f4ea2a402 Merge pull request #1325 from sdurrheimer/master
Zsh completion for docker-compose
(cherry picked from commit b638728d6c)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>

Conflicts:
	docs/completion.md
2015-06-09 14:39:50 -04:00
Aanand Prasad
5a5bffebd1 Merge pull request #1464 from twhiteman/bug1461
Possible division by zero error when pulling an image - fixes #1463
(cherry picked from commit d0e87929a1)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-09 14:39:05 -04:00
Aanand Prasad
8749bc0844 Build Python 2.7.9 in Docker image
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-02 11:39:17 +01:00
Aanand Prasad
f3d0c63db2 Make sure we use Python 2.7.9 and OpenSSL 1.0.1 when building OSX binary
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-06-02 11:39:17 +01:00
Aanand Prasad
93a846db31 Report Python and OpenSSL versions in --version output
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>

Conflicts:
	compose/cli/utils.py
2015-06-02 11:39:17 +01:00
Aanand Prasad
686c25d50f Script to prepare OSX build environment
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-05-27 17:52:23 +01:00
Aanand Prasad
ef6555f084 Merge branch 'release' into bump-1.3.0 2015-05-26 17:45:28 +01:00
Ben Firshman
3c6652c101 Merge pull request #1308 from aanand/update-docs-1.2.0
Update docs for 1.2.0
2015-04-17 10:35:06 -07:00
Aanand Prasad
43af1684c1 Update docs for 1.2.0
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-04-17 16:02:57 +01:00
Aanand Prasad
ed549155b3 Merge pull request #1159 from aanand/bump-1.2.0
Bump 1.2.0
2015-04-16 17:46:47 +01:00
Aanand Prasad
39ae91c81c Bump 1.2.0
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-04-16 13:53:27 +01:00
Daniel Nephin
b6acb3cd8c Merge pull request #1278 from albers/completion-run-user
Add bash completion for docker-compose run --user
(cherry picked from commit 3cd116b99d)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-04-16 13:50:33 +01:00
Ben Firshman
a467a8a094 Merge pull request #1261 from aanand/fix-vars-in-volume-paths
Fix vars in volume paths
(cherry picked from commit 4926f8aef6)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>

Conflicts:
	tests/unit/service_test.py
2015-04-09 15:26:10 +01:00
Aanand Prasad
78227c3c06 Merge pull request #1202 from aanand/jenkins-script
WIP: Jenkins script
(cherry picked from commit 853ce255ea)
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-04-01 11:00:51 -04:00
Daniel Nephin
e4e802d1f8 Merge pull request #1213 from moysesb/relative_build
Make value of 'build:' relative to the yml file.
(cherry picked from commit 0f70b8638f)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-04-01 11:00:48 -04:00
Aanand Prasad
b24a60ba9f Merge pull request #1226 from aanand/merge-multi-value-options
Merge multi-value options when extending
(cherry picked from commit e708f4f59d)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-04-01 11:00:45 -04:00
Aanand Prasad
461b600068 Merge pull request #1225 from aanand/fix-1222
When extending, `build` replaces `image` and vice versa
(cherry picked from commit 6dbe321a45)

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2015-04-01 11:00:40 -04:00
190 changed files with 6173 additions and 9448 deletions

View file

@ -10,7 +10,7 @@
- id: end-of-file-fixer - id: end-of-file-fixer
- id: flake8 - id: flake8
- id: name-tests-test - id: name-tests-test
exclude: 'tests/(integration/testcases\.py|helpers\.py)' exclude: 'tests/integration/testcases.py'
- id: requirements-txt-fixer - id: requirements-txt-fixer
- id: trailing-whitespace - id: trailing-whitespace
- repo: git://github.com/asottile/reorder_python_imports - repo: git://github.com/asottile/reorder_python_imports

View file

@ -25,5 +25,3 @@ deploy:
key: '$BINTRAY_API_KEY' key: '$BINTRAY_API_KEY'
file: ./bintray.json file: ./bintray.json
skip_cleanup: true skip_cleanup: true
on:
all_branches: true

View file

@ -1,398 +1,6 @@
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)
-----------------
**Breaking changes**
- When using Compose with Docker Toolbox/Machine on Windows, volume paths are
no longer converted from `C:\Users` to `/c/Users`-style by default. To
re-enable this conversion so that your volumes keep working, set the
environment variable `COMPOSE_CONVERT_WINDOWS_PATHS=1`. Users of
Docker for Windows are not affected and do not need to set the variable.
New Features
- Interactive mode for `docker-compose run` and `docker-compose exec` is
now supported on Windows platforms. Please note that the `docker` binary
is required to be present on the system for this feature to work.
- Introduced version 2.1 of the `docker-compose.yml` specification. This
version requires to be used with Docker Engine 1.12 or above.
- Added support for setting volume labels and network labels in
`docker-compose.yml`.
- Added support for the `isolation` parameter in service definitions.
- Added support for link-local IPs in the service networks definitions.
- Added support for shell-style inline defaults in variable interpolation.
The supported forms are `${FOO-default}` (fall back if FOO is unset) and
`${FOO:-default}` (fall back if FOO is unset or empty).
- Added support for the `group_add` and `oom_score_adj` parameters in
service definitions.
- Added support for the `internal` and `enable_ipv6` parameters in network
definitions.
- Compose now defaults to using the `npipe` protocol on Windows.
- Overriding a `logging` configuration will now properly merge the `options`
mappings if the `driver` values do not conflict.
Bug Fixes
- Fixed several bugs related to `npipe` protocol support on Windows.
- Fixed an issue with Windows paths being incorrectly converted when
using Docker on Windows Server.
- Fixed a bug where an empty `restart` value would sometimes result in an
exception being raised.
- Fixed an issue where service logs containing unicode characters would
sometimes cause an error to occur.
- Fixed a bug where unicode values in environment variables would sometimes
raise a unicode exception when retrieved.
- Fixed an issue where Compose would incorrectly detect a configuration
mismatch for overlay networks.
1.8.1 (2016-09-22)
-----------------
Bug Fixes
- Fixed a bug where users using a credentials store were not able
to access their private images.
- Fixed a bug where users using identity tokens to authenticate
were not able to access their private images.
- Fixed a bug where an `HttpHeaders` entry in the docker configuration
file would cause Compose to crash when trying to build an image.
- Fixed a few bugs related to the handling of Windows paths in volume
binding declarations.
- Fixed a bug where Compose would sometimes crash while trying to
read a streaming response from the engine.
- Fixed an issue where Compose would crash when encountering an API error
while streaming container logs.
- Fixed an issue where Compose would erroneously try to output logs from
drivers not handled by the Engine's API.
- Fixed a bug where options from the `docker-machine config` command would
not be properly interpreted by Compose.
- Fixed a bug where the connection to the Docker Engine would
sometimes fail when running a large number of services simultaneously.
- Fixed an issue where Compose would sometimes print a misleading
suggestion message when running the `bundle` command.
- Fixed a bug where connection errors would not be handled properly by
Compose during the project initialization phase.
- Fixed a bug where a misleading error would appear when encountering
a connection timeout.
1.8.0 (2016-06-14)
-----------------
**Breaking Changes**
- As announced in 1.7.0, `docker-compose rm` now removes containers
created by `docker-compose run` by default.
- Setting `entrypoint` on a service now empties out any default
command that was set on the image (i.e. any `CMD` instruction in the
Dockerfile used to build it). This makes it consistent with
the `--entrypoint` flag to `docker run`.
New Features
- Added `docker-compose bundle`, a command that builds a bundle file
to be consumed by the new *Docker Stack* commands in Docker 1.12.
- Added `docker-compose push`, a command that pushes service images
to a registry.
- Compose now supports specifying a custom TLS version for
interaction with the Docker Engine using the `COMPOSE_TLS_VERSION`
environment variable.
Bug Fixes
- Fixed a bug where Compose would erroneously try to read `.env`
at the project's root when it is a directory.
- `docker-compose run -e VAR` now passes `VAR` through from the shell
to the container, as with `docker run -e VAR`.
- Improved config merging when multiple compose files are involved
for several service sub-keys.
- Fixed a bug where volume mappings containing Windows drives would
sometimes be parsed incorrectly.
- Fixed a bug in Windows environment where volume mappings of the
host's root directory would be parsed incorrectly.
- Fixed a bug where `docker-compose config` would output an invalid
Compose file if external networks were specified.
- Fixed an issue where unset buildargs would be assigned a string
containing `'None'` instead of the expected empty value.
- Fixed a bug where yes/no prompts on Windows would not show before
receiving input.
- Fixed a bug where trying to `docker-compose exec` on Windows
without the `-d` option would exit with a stacktrace. This will
still fail for the time being, but should do so gracefully.
- Fixed a bug where errors during `docker-compose up` would show
an unrelated stacktrace at the end of the process.
- `docker-compose create` and `docker-compose start` show more
descriptive error messages when something goes wrong.
1.7.1 (2016-05-04)
-----------------
Bug Fixes
- Fixed a bug where the output of `docker-compose config` for v1 files
would be an invalid configuration file.
- Fixed a bug where `docker-compose config` would not check the validity
of links.
- Fixed an issue where `docker-compose help` would not output a list of
available commands and generic options as expected.
- Fixed an issue where filtering by service when using `docker-compose logs`
would not apply for newly created services.
- Fixed a bug where unchanged services would sometimes be recreated in
in the up phase when using Compose with Python 3.
- Fixed an issue where API errors encountered during the up phase would
not be recognized as a failure state by Compose.
- Fixed a bug where Compose would raise a NameError because of an undefined
exception name on non-Windows platforms.
- Fixed a bug where the wrong version of `docker-py` would sometimes be
installed alongside Compose.
- Fixed a bug where the host value output by `docker-machine config default`
would not be recognized as valid options by the `docker-compose`
command line.
- Fixed an issue where Compose would sometimes exit unexpectedly while
reading events broadcasted by a Swarm cluster.
- Corrected a statement in the docs about the location of the `.env` file,
which is indeed read from the current directory, instead of in the same
location as the Compose file.
1.7.0 (2016-04-13)
------------------
**Breaking Changes**
- `docker-compose logs` no longer follows log output by default. It now
matches the behaviour of `docker logs` and exits after the current logs
are printed. Use `-f` to get the old default behaviour.
- Booleans are no longer allows as values for mappings in the Compose file
(for keys `environment`, `labels` and `extra_hosts`). Previously this
was a warning. Boolean values should be quoted so they become string values.
New Features
- Compose now looks for a `.env` file in the directory where it's run and
reads any environment variables defined inside, if they're not already
set in the shell environment. This lets you easily set defaults for
variables used in the Compose file, or for any of the `COMPOSE_*` or
`DOCKER_*` variables.
- Added a `--remove-orphans` flag to both `docker-compose up` and
`docker-compose down` to remove containers for services that were removed
from the Compose file.
- Added a `--all` flag to `docker-compose rm` to include containers created
by `docker-compose run`. This will become the default behavior in the next
version of Compose.
- Added support for all the same TLS configuration flags used by the `docker`
client: `--tls`, `--tlscert`, `--tlskey`, etc.
- Compose files now support the `tmpfs` and `shm_size` options.
- Added the `--workdir` flag to `docker-compose run`
- `docker-compose logs` now shows logs for new containers that are created
after it starts.
- The `COMPOSE_FILE` environment variable can now contain multiple files,
separated by the host system's standard path separator (`:` on Mac/Linux,
`;` on Windows).
- You can now specify a static IP address when connecting a service to a
network with the `ipv4_address` and `ipv6_address` options.
- Added `--follow`, `--timestamp`, and `--tail` flags to the
`docker-compose logs` command.
- `docker-compose up`, and `docker-compose start` will now start containers
in parallel where possible.
- `docker-compose stop` now stops containers in reverse dependency order
instead of all at once.
- Added the `--build` flag to `docker-compose up` to force it to build a new
image. It now shows a warning if an image is automatically built when the
flag is not used.
- Added the `docker-compose exec` command for executing a process in a running
container.
Bug Fixes
- `docker-compose down` now removes containers created by
`docker-compose run`.
- A more appropriate error is shown when a timeout is hit during `up` when
using a tty.
- Fixed a bug in `docker-compose down` where it would abort if some resources
had already been removed.
- Fixed a bug where changes to network aliases would not trigger a service
to be recreated.
- Fix a bug where a log message was printed about creating a new volume
when it already existed.
- Fixed a bug where interrupting `up` would not always shut down containers.
- Fixed a bug where `log_opt` and `log_driver` were not properly carried over
when extending services in the v1 Compose file format.
- Fixed a bug where empty values for build args would cause file validation
to fail.
1.6.2 (2016-02-23)
------------------
- Fixed a bug where connecting to a TLS-enabled Docker Engine would fail with
a certificate verification error.
1.6.1 (2016-02-23) 1.6.1 (2016-02-23)
------------------ ------------------
@ -620,7 +228,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 hiding logs. each line to prevent buffering issues from hideing 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
@ -909,7 +517,7 @@ Fig has been renamed to Docker Compose, or just Compose for short. This has seve
- The command you type is now `docker-compose`, not `fig`. - The command you type is now `docker-compose`, not `fig`.
- You should rename your fig.yml to docker-compose.yml. - You should rename your fig.yml to docker-compose.yml.
- If youre installing via PyPI, the package is now `docker-compose`, so install it with `pip install docker-compose`. - If youre installing via PyPi, the package is now `docker-compose`, so install it with `pip install docker-compose`.
Besides that, theres a lot of new stuff in this release: Besides that, theres a lot of new stuff in this release:

View file

@ -35,7 +35,7 @@ that should get you started.
This step is optional, but recommended. Pre-commit hooks will run style checks This step is optional, but recommended. Pre-commit hooks will run style checks
and in some cases fix style issues for you, when you commit code. and in some cases fix style issues for you, when you commit code.
Install the git pre-commit hooks using [tox](https://tox.readthedocs.io) by Install the git pre-commit hooks using [tox](https://tox.readthedocs.org) by
running `tox -e pre-commit` or by following the running `tox -e pre-commit` or by following the
[pre-commit install guide](http://pre-commit.com/#install). [pre-commit install guide](http://pre-commit.com/#install).
@ -50,22 +50,22 @@ See Docker's [basic contribution workflow](https://docs.docker.com/opensource/wo
Use the test script to run linting checks and then the full test suite against Use the test script to run linting checks and then the full test suite against
different Python interpreters: different Python interpreters:
$ script/test/default $ script/test
Tests are run against a Docker daemon inside a container, so that we can test Tests are run against a Docker daemon inside a container, so that we can test
against multiple Docker versions. By default they'll run against only the latest against multiple Docker versions. By default they'll run against only the latest
Docker version - set the `DOCKER_VERSIONS` environment variable to "all" to run Docker version - set the `DOCKER_VERSIONS` environment variable to "all" to run
against all supported versions: against all supported versions:
$ DOCKER_VERSIONS=all script/test/default $ DOCKER_VERSIONS=all script/test
Arguments to `script/test/default` are passed through to the `tox` executable, so Arguments to `script/test` are passed through to the `nosetests` executable, so
you can specify a test directory, file, module, class or method: you can specify a test directory, file, module, class or method:
$ script/test/default tests/unit $ script/test tests/unit
$ script/test/default tests/unit/cli_test.py $ script/test tests/unit/cli_test.py
$ script/test/default tests/unit/config_test.py::ConfigTest $ script/test tests/unit/config_test.py::ConfigTest
$ script/test/default tests/unit/config_test.py::ConfigTest::test_load $ script/test tests/unit/config_test.py::ConfigTest::test_load
## Finding things to work on ## Finding things to work on

View file

@ -13,7 +13,6 @@ 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/*
@ -21,32 +20,40 @@ 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.13 from source # Build Python 2.7.9 from source
RUN set -ex; \ RUN set -ex; \
curl -L https://www.python.org/ftp/python/2.7.13/Python-2.7.13.tgz | tar -xz; \ curl -L https://www.python.org/ftp/python/2.7.9/Python-2.7.9.tgz | tar -xz; \
cd Python-2.7.13; \ cd Python-2.7.9; \
./configure --enable-shared; \ ./configure --enable-shared; \
make; \ make; \
make install; \ make install; \
cd ..; \ cd ..; \
rm -rf /Python-2.7.13 rm -rf /Python-2.7.9
# 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.6/Python-3.4.6.tgz | tar -xz; \ curl -L https://www.python.org/ftp/python/3.4.3/Python-3.4.3.tgz | tar -xz; \
cd Python-3.4.6; \ cd Python-3.4.3; \
./configure --enable-shared; \ ./configure --enable-shared; \
make; \ make; \
make install; \ make install; \
cd ..; \ cd ..; \
rm -rf /Python-3.4.6 rm -rf /Python-3.4.3
# 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://bootstrap.pypa.io/get-pip.py | python curl -L https://pypi.python.org/packages/source/p/pip/pip-7.0.1.tar.gz | tar -xz; \
cd pip-7.0.1; \
python setup.py install; \
cd ..; \
rm -rf pip-7.0.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,6 +1,5 @@
FROM alpine:3.4 FROM alpine:edge
ARG version
RUN apk -U add \ RUN apk -U add \
python \ python \
py-pip py-pip
@ -8,7 +7,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
COPY dist/docker_compose-${version}-py2.py3-none-any.whl /code/ ADD dist/docker-compose-release.tar.gz /code/docker-compose
RUN pip install --no-deps /code/docker_compose-${version}-py2.py3-none-any.whl RUN pip install --no-deps /code/docker-compose/docker-compose-*
ENTRYPOINT ["/usr/bin/docker-compose"] ENTRYPOINT ["/usr/bin/docker-compose"]

64
Jenkinsfile vendored
View file

@ -1,64 +0,0 @@
#!groovy
def image
def buildImage = { ->
wrappedNode(label: "ubuntu && !zfs", cleanWorkspace: true) {
stage("build image") {
checkout(scm)
def imageName = "dockerbuildbot/compose:${gitCommit()}"
image = docker.image(imageName)
try {
image.pull()
} catch (Exception exc) {
image = docker.build(imageName, ".")
image.push()
}
}
}
}
def runTests = { Map settings ->
def dockerVersions = settings.get("dockerVersions", null)
def pythonVersions = settings.get("pythonVersions", null)
if (!pythonVersions) {
throw new Exception("Need Python versions to test. e.g.: `runTests(pythonVersions: 'py27,py34')`")
}
if (!dockerVersions) {
throw new Exception("Need Docker versions to test. e.g.: `runTests(dockerVersions: 'all')`")
}
{ ->
wrappedNode(label: "ubuntu && !zfs", cleanWorkspace: true) {
stage("test python=${pythonVersions} / docker=${dockerVersions}") {
checkout(scm)
def storageDriver = sh(script: 'docker info | awk -F \': \' \'$1 == "Storage Driver" { print $2; exit }\'', returnStdout: true).trim()
echo "Using local system's storage driver: ${storageDriver}"
sh """docker run \\
-t \\
--rm \\
--privileged \\
--volume="\$(pwd)/.git:/code/.git" \\
--volume="/var/run/docker.sock:/var/run/docker.sock" \\
-e "TAG=${image.id}" \\
-e "STORAGE_DRIVER=${storageDriver}" \\
-e "DOCKER_VERSIONS=${dockerVersions}" \\
-e "BUILD_NUMBER=\$BUILD_TAG" \\
-e "PY_TEST_VERSIONS=${pythonVersions}" \\
--entrypoint="script/ci" \\
${image.id} \\
--verbose
"""
}
}
}
}
buildImage()
// TODO: break this out into meaningful "DOCKER_VERSIONS" values instead of all
parallel(
failFast: true,
all_py27: runTests(pythonVersions: "py27", dockerVersions: "all"),
all_py34: runTests(pythonVersions: "py34", dockerVersions: "all"),
)

View file

@ -6,11 +6,11 @@ Compose is a tool for defining and running multi-container Docker applications.
With Compose, you use a Compose file to configure your application's services. With Compose, you use a Compose file to configure your application's services.
Then, using a single command, you create and start all the services Then, using a single command, you create and start all the services
from your configuration. To learn more about all the features of Compose from your configuration. To learn more about all the features of Compose
see [the list of features](https://github.com/docker/docker.github.io/blob/master/compose/overview.md#features). see [the list of features](https://github.com/docker/compose/blob/release/docs/overview.md#features).
Compose is great for development, testing, and staging environments, as well as Compose is great for development, testing, and staging environments, as well as
CI workflows. You can learn more about each case in CI workflows. You can learn more about each case in
[Common Use Cases](https://github.com/docker/docker.github.io/blob/master/compose/overview.md#common-use-cases). [Common Use Cases](https://github.com/docker/compose/blob/release/docs/overview.md#common-use-cases).
Using Compose is basically a three-step process. Using Compose is basically a three-step process.
@ -22,20 +22,19 @@ they can be run together in an isolated environment:
A `docker-compose.yml` looks like this: A `docker-compose.yml` looks like this:
version: '2' web:
build: .
services: ports:
web: - "5000:5000"
build: . volumes:
ports: - .:/code
- "5000:5000" links:
volumes: - redis
- .:/code redis:
redis: 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/compose-versioning.md) [Compose file reference](https://github.com/docker/compose/blob/release/docs/compose-file.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,21 +1,13 @@
# Roadmap # Roadmap
## An even better tool for development environments
Compose is a great tool for development environments, but it could be even better. For example:
- It should be possible to define hostnames for containers which work from the host machine, e.g. “mywebcontainer.local”. This is needed by apps comprising multiple web services which generate links to one another (e.g. a frontend website and a separate admin webapp)
## More than just development environments ## More than just development environments
Compose currently works really well in development, but we want to make the Compose file format better for test, staging, and production environments. To support these use cases, there will need to be improvements to the file format, improvements to the command-line tool, integrations with other tools, and perhaps new tools altogether. Over time we will extend Compose's remit to cover test, staging and production environments. This is not a simple task, and will take many incremental improvements such as:
Some specific things we are considering:
- Compose currently will attempt to get your application into the correct state when running `up`, but it has a number of shortcomings: - Compose currently will attempt to get your application into the correct state when running `up`, but it has a number of shortcomings:
- It should roll back to a known good state if it fails. - It should roll back to a known good state if it fails.
- It should allow a user to check the actions it is about to perform before running them. - It should allow a user to check the actions it is about to perform before running them.
- It should be possible to partially modify the config file for different environments (dev/test/staging/prod), passing in e.g. custom ports, volume mount paths, or volume drivers. ([#1377](https://github.com/docker/compose/issues/1377)) - It should be possible to partially modify the config file for different environments (dev/test/staging/prod), passing in e.g. custom ports or volume mount paths. ([#1377](https://github.com/docker/compose/issues/1377))
- Compose should recommend a technique for zero-downtime deploys. - Compose should recommend a technique for zero-downtime deploys.
- It should be possible to continuously attempt to keep an application in the correct state, instead of just performing `up` a single time. - It should be possible to continuously attempt to keep an application in the correct state, instead of just performing `up` a single time.
@ -30,3 +22,10 @@ The current state of integration is documented in [SWARM.md](SWARM.md).
Compose works well for applications that are in a single repository and depend on services that are hosted on Docker Hub. If your application depends on another application within your organisation, Compose doesn't work as well. Compose works well for applications that are in a single repository and depend on services that are hosted on Docker Hub. If your application depends on another application within your organisation, Compose doesn't work as well.
There are several ideas about how this could work, such as [including external files](https://github.com/docker/fig/issues/318). There are several ideas about how this could work, such as [including external files](https://github.com/docker/fig/issues/318).
## An even better tool for development environments
Compose is a great tool for development environments, but it could be even better. For example:
- [Compose could watch your code and automatically kick off builds when something changes.](https://github.com/docker/fig/issues/184)
- It should be possible to define hostnames for containers which work from the host machine, e.g. “mywebcontainer.local”. This is needed by apps comprising multiple web services which generate links to one another (e.g. a frontend website and a separate admin webapp)

View file

@ -9,16 +9,20 @@ install:
# Build the binary after tests # Build the binary after tests
build: false build: false
environment:
BINTRAY_USER: "docker-compose-roleuser"
BINTRAY_PATH: "docker-compose/master/windows/master/docker-compose-Windows-x86_64.exe"
test_script: test_script:
- "tox -e py27,py34 -- tests/unit" - "tox -e py27,py34 -- tests/unit"
- ps: ".\\script\\build\\windows.ps1" - ps: ".\\script\\build-windows.ps1"
deploy_script:
- "curl -sS
-u \"%BINTRAY_USER%:%BINTRAY_API_KEY%\"
-X PUT \"https://api.bintray.com/content/%BINTRAY_PATH%?override=1&publish=1\"
--data-binary @dist\\docker-compose-Windows-x86_64.exe"
artifacts: artifacts:
- path: .\dist\docker-compose-Windows-x86_64.exe - path: .\dist\docker-compose-Windows-x86_64.exe
name: "Compose Windows binary" name: "Compose Windows binary"
deploy:
- provider: Environment
name: master-builds
on:
branch: master

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.12.0dev' __version__ = '1.6.1'

View file

@ -1,258 +0,0 @@
from __future__ import absolute_import
from __future__ import unicode_literals
import json
import logging
import six
from docker.utils import split_command
from docker.utils.ports import split_port
from .cli.errors import UserError
from .config.serialize import denormalize_config
from .network import get_network_defs_for_service
from .service import format_environment
from .service import NoSuchImageError
from .service import parse_repository_tag
log = logging.getLogger(__name__)
SERVICE_KEYS = {
'working_dir': 'WorkingDir',
'user': 'User',
'labels': 'Labels',
}
IGNORED_KEYS = {'build'}
SUPPORTED_KEYS = {
'image',
'ports',
'expose',
'networks',
'command',
'environment',
'entrypoint',
} | set(SERVICE_KEYS)
VERSION = '0.1'
class NeedsPush(Exception):
def __init__(self, image_name):
self.image_name = image_name
class NeedsPull(Exception):
def __init__(self, image_name, service_name):
self.image_name = image_name
self.service_name = service_name
class MissingDigests(Exception):
def __init__(self, needs_push, needs_pull):
self.needs_push = needs_push
self.needs_pull = needs_pull
def serialize_bundle(config, image_digests):
return json.dumps(to_bundle(config, image_digests), indent=2, sort_keys=True)
def get_image_digests(project, allow_push=False):
digests = {}
needs_push = set()
needs_pull = set()
for service in project.services:
try:
digests[service.name] = get_image_digest(
service,
allow_push=allow_push,
)
except NeedsPush as e:
needs_push.add(e.image_name)
except NeedsPull as e:
needs_pull.add(e.service_name)
if needs_push or needs_pull:
raise MissingDigests(needs_push, needs_pull)
return digests
def get_image_digest(service, allow_push=False):
if 'image' not in service.options:
raise UserError(
"Service '{s.name}' doesn't define an image tag. An image name is "
"required to generate a proper image digest for the bundle. Specify "
"an image repo and tag with the 'image' option.".format(s=service))
_, _, separator = parse_repository_tag(service.options['image'])
# Compose file already uses a digest, no lookup required
if separator == '@':
return service.options['image']
try:
image = service.image()
except NoSuchImageError:
action = 'build' if 'build' in service.options else 'pull'
raise UserError(
"Image not found for service '{service}'. "
"You might need to run `docker-compose {action} {service}`."
.format(service=service.name, action=action))
if image['RepoDigests']:
# TODO: pick a digest based on the image tag if there are multiple
# digests
return image['RepoDigests'][0]
if 'build' not in service.options:
raise NeedsPull(service.image_name, service.name)
if not allow_push:
raise NeedsPush(service.image_name)
return push_image(service)
def push_image(service):
try:
digest = service.push()
except:
log.error(
"Failed to push image for service '{s.name}'. Please use an "
"image tag that can be pushed to a Docker "
"registry.".format(s=service))
raise
if not digest:
raise ValueError("Failed to get digest for %s" % service.name)
repo, _, _ = parse_repository_tag(service.options['image'])
identifier = '{repo}@{digest}'.format(repo=repo, digest=digest)
# only do this if RepoDigests isn't already populated
image = service.image()
if not image['RepoDigests']:
# Pull by digest so that image['RepoDigests'] is populated for next time
# and we don't have to pull/push again
service.client.pull(identifier)
log.info("Stored digest for {}".format(service.image_name))
return identifier
def to_bundle(config, image_digests):
if config.networks:
log.warn("Unsupported top level key 'networks' - ignoring")
if config.volumes:
log.warn("Unsupported top level key 'volumes' - ignoring")
config = denormalize_config(config)
return {
'Version': VERSION,
'Services': {
name: convert_service_to_bundle(
name,
service_dict,
image_digests[name],
)
for name, service_dict in config['services'].items()
},
}
def convert_service_to_bundle(name, service_dict, image_digest):
container_config = {'Image': image_digest}
for key, value in service_dict.items():
if key in IGNORED_KEYS:
continue
if key not in SUPPORTED_KEYS:
log.warn("Unsupported key '{}' in services.{} - ignoring".format(key, name))
continue
if key == 'environment':
container_config['Env'] = format_environment({
envkey: envvalue for envkey, envvalue in value.items()
if envvalue
})
continue
if key in SERVICE_KEYS:
container_config[SERVICE_KEYS[key]] = value
continue
set_command_and_args(
container_config,
service_dict.get('entrypoint', []),
service_dict.get('command', []))
container_config['Networks'] = make_service_networks(name, service_dict)
ports = make_port_specs(service_dict)
if ports:
container_config['Ports'] = ports
return container_config
# See https://github.com/docker/swarmkit/blob/agent/exec/container/container.go#L95
def set_command_and_args(config, entrypoint, command):
if isinstance(entrypoint, six.string_types):
entrypoint = split_command(entrypoint)
if isinstance(command, six.string_types):
command = split_command(command)
if entrypoint:
config['Command'] = entrypoint + command
return
if command:
config['Args'] = command
def make_service_networks(name, service_dict):
networks = []
for network_name, network_def in get_network_defs_for_service(service_dict).items():
for key in network_def.keys():
log.warn(
"Unsupported key '{}' in services.{}.networks.{} - ignoring"
.format(key, name, network_name))
networks.append(network_name)
return networks
def make_port_specs(service_dict):
ports = []
internal_ports = [
internal_port
for port_def in service_dict.get('ports', [])
for internal_port in split_port(port_def)[0]
]
internal_ports += service_dict.get('expose', [])
for internal_port in internal_ports:
spec = make_port_spec(internal_port)
if spec not in ports:
ports.append(spec)
return ports
def make_port_spec(value):
components = six.text_type(value).partition('/')
return {
'Protocol': components[2] or 'tcp',
'Port': int(components[0]),
}

View file

@ -1,37 +0,0 @@
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

@ -1,8 +1,5 @@
from __future__ import absolute_import from __future__ import absolute_import
from __future__ import unicode_literals from __future__ import unicode_literals
import colorama
NAMES = [ NAMES = [
'grey', 'grey',
'red', 'red',
@ -33,7 +30,6 @@ def make_color_fn(code):
return lambda s: ansi_color(code, s) return lambda s: ansi_color(code, s)
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

@ -1,87 +1,69 @@
from __future__ import absolute_import from __future__ import absolute_import
from __future__ import unicode_literals from __future__ import unicode_literals
import contextlib
import logging import logging
import os import os
import re import re
import ssl
import six import six
from requests.exceptions import ConnectionError
from requests.exceptions import SSLError
from . import errors from . import errors
from . import verbose_proxy from . import verbose_proxy
from .. import config from .. import config
from ..config.environment import Environment
from ..const import API_VERSIONS from ..const import API_VERSIONS
from ..project import Project from ..project import Project
from .docker_client import docker_client from .docker_client import docker_client
from .docker_client import tls_config_from_options from .utils import call_silently
from .utils import get_version_info from .utils import get_version_info
from .utils import is_mac
from .utils import is_ubuntu
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def project_from_options(project_dir, options): @contextlib.contextmanager
environment = Environment.from_env_file(project_dir) def friendly_error_message():
host = options.get('--host') try:
if host is not None: yield
host = host.lstrip('=') except SSLError as e:
raise errors.UserError('SSL error: %s' % e)
except ConnectionError:
if call_silently(['which', 'docker']) != 0:
if is_mac():
raise errors.DockerNotFoundMac()
elif is_ubuntu():
raise errors.DockerNotFoundUbuntu()
else:
raise errors.DockerNotFoundGeneric()
elif call_silently(['which', 'docker-machine']) == 0:
raise errors.ConnectionErrorDockerMachine()
else:
raise errors.ConnectionErrorGeneric(get_client().base_url)
def project_from_options(base_dir, options):
return get_project( return get_project(
project_dir, base_dir,
get_config_path_from_options(project_dir, options, environment), get_config_path_from_options(options),
project_name=options.get('--project-name'), project_name=options.get('--project-name'),
verbose=options.get('--verbose'), verbose=options.get('--verbose'),
host=host,
tls_config=tls_config_from_options(options),
environment=environment
) )
def get_config_from_options(base_dir, options): def get_config_path_from_options(options):
environment = Environment.from_env_file(base_dir)
config_path = get_config_path_from_options(
base_dir, options, environment
)
return config.load(
config.find(base_dir, config_path, environment)
)
def get_config_path_from_options(base_dir, options, environment):
file_option = options.get('--file') file_option = options.get('--file')
if file_option: if file_option:
return file_option return file_option
config_files = environment.get('COMPOSE_FILE') config_file = os.environ.get('COMPOSE_FILE')
if config_files: return [config_file] if config_file else None
return config_files.split(os.pathsep)
return None
def get_tls_version(environment): def get_client(verbose=False, version=None):
compose_tls_version = environment.get('COMPOSE_TLS_VERSION', None) client = docker_client(version=version)
if not compose_tls_version:
return None
tls_attr_name = "PROTOCOL_{}".format(compose_tls_version)
if not hasattr(ssl, tls_attr_name):
log.warn(
'The "{}" protocol is unavailable. You may need to update your '
'version of Python or OpenSSL. Falling back to TLSv1 (default).'
.format(compose_tls_version)
)
return None
return getattr(ssl, tls_attr_name)
def get_client(environment, verbose=False, version=None, tls_config=None, host=None,
tls_version=None):
client = docker_client(
version=version, tls_config=tls_config, host=host,
environment=environment, tls_version=get_tls_version(environment)
)
if verbose: if verbose:
version_info = six.iteritems(client.version()) version_info = six.iteritems(client.version())
log.info(get_version_info('full')) log.info(get_version_info('full'))
@ -92,36 +74,24 @@ def get_client(environment, verbose=False, version=None, tls_config=None, host=N
return client return client
def get_project(project_dir, config_path=None, project_name=None, verbose=False, def get_project(base_dir, config_path=None, project_name=None, verbose=False):
host=None, tls_config=None, environment=None): config_details = config.find(base_dir, config_path)
if not environment: project_name = get_project_name(config_details.working_dir, project_name)
environment = Environment.from_env_file(project_dir)
config_details = config.find(project_dir, config_path, environment)
project_name = get_project_name(
config_details.working_dir, project_name, environment
)
config_data = config.load(config_details) config_data = config.load(config_details)
api_version = environment.get( api_version = os.environ.get(
'COMPOSE_API_VERSION', 'COMPOSE_API_VERSION',
API_VERSIONS[config_data.version]) API_VERSIONS[config_data.version])
client = get_client(verbose=verbose, version=api_version)
client = get_client( return Project.from_config(project_name, config_data, client)
verbose=verbose, version=api_version, tls_config=tls_config,
host=host, environment=environment
)
with errors.handle_connection_errors(client):
return Project.from_config(project_name, config_data, client)
def get_project_name(working_dir, project_name=None, environment=None): def get_project_name(working_dir, project_name=None):
def normalize_name(name): def normalize_name(name):
return re.sub(r'[^a-z0-9]', '', name.lower()) return re.sub(r'[^a-z0-9]', '', name.lower())
if not environment: project_name = project_name or os.environ.get('COMPOSE_PROJECT_NAME')
environment = Environment.from_env_file(working_dir)
project_name = project_name or environment.get('COMPOSE_PROJECT_NAME')
if project_name: if project_name:
return normalize_name(project_name) return normalize_name(project_name)

View file

@ -2,73 +2,36 @@ from __future__ import absolute_import
from __future__ import unicode_literals from __future__ import unicode_literals
import logging import logging
import os
from docker import APIClient from docker import Client
from docker.errors import TLSParameterError from docker.errors import TLSParameterError
from docker.tls import TLSConfig
from docker.utils import kwargs_from_env from docker.utils import kwargs_from_env
from ..const import HTTP_TIMEOUT from ..const import HTTP_TIMEOUT
from .errors import UserError from .errors import UserError
from .utils import generate_user_agent
from .utils import unquote_path
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def tls_config_from_options(options): def docker_client(version=None):
tls = options.get('--tls', False)
ca_cert = unquote_path(options.get('--tlscacert'))
cert = unquote_path(options.get('--tlscert'))
key = unquote_path(options.get('--tlskey'))
verify = options.get('--tlsverify')
skip_hostname_check = options.get('--skip-hostname-check', False)
advanced_opts = any([ca_cert, cert, key, verify])
if tls is True and not advanced_opts:
return True
elif advanced_opts: # --tls is a noop
client_cert = None
if cert or key:
client_cert = (cert, key)
return TLSConfig(
client_cert=client_cert, verify=verify, ca_cert=ca_cert,
assert_hostname=False if skip_hostname_check else None
)
return None
def docker_client(environment, version=None, tls_config=None, host=None,
tls_version=None):
""" """
Returns a docker-py client configured using environment variables Returns a docker-py client configured using environment variables
according to the same logic as the official Docker client. according to the same logic as the official Docker client.
""" """
if 'DOCKER_CLIENT_TIMEOUT' in os.environ:
log.warn('The DOCKER_CLIENT_TIMEOUT environment variable is deprecated. Please use COMPOSE_HTTP_TIMEOUT instead.')
try: try:
kwargs = kwargs_from_env(environment=environment, ssl_version=tls_version) kwargs = kwargs_from_env(assert_hostname=False)
except TLSParameterError: except TLSParameterError:
raise UserError( raise UserError(
"TLS configuration is invalid - make sure your DOCKER_TLS_VERIFY " 'TLS configuration is invalid - make sure your DOCKER_TLS_VERIFY and DOCKER_CERT_PATH are set correctly.\n'
"and DOCKER_CERT_PATH are set correctly.\n" 'You might need to run `eval "$(docker-machine env default)"`')
"You might need to run `eval \"$(docker-machine env default)\"`")
if host:
kwargs['base_url'] = host
if tls_config:
kwargs['tls'] = tls_config
if version: if version:
kwargs['version'] = version kwargs['version'] = version
timeout = environment.get('COMPOSE_HTTP_TIMEOUT') kwargs['timeout'] = HTTP_TIMEOUT
if timeout:
kwargs['timeout'] = int(timeout)
else:
kwargs['timeout'] = HTTP_TIMEOUT
kwargs['user_agent'] = generate_user_agent() return Client(**kwargs)
return APIClient(**kwargs)

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 sys
from inspect import getdoc from inspect import getdoc
from docopt import docopt from docopt import docopt
@ -14,21 +15,24 @@ def docopt_full_help(docstring, *args, **kwargs):
raise SystemExit(docstring) raise SystemExit(docstring)
class DocoptDispatcher(object): class DocoptCommand(object):
def docopt_options(self):
return {'options_first': True}
def __init__(self, command_class, options): def sys_dispatch(self):
self.command_class = command_class self.dispatch(sys.argv[1:], None)
self.options = options
def parse(self, argv): def dispatch(self, argv, global_options):
command_help = getdoc(self.command_class) self.perform_command(*self.parse(argv, global_options))
options = docopt_full_help(command_help, argv, **self.options)
def parse(self, argv, global_options):
options = docopt_full_help(getdoc(self), argv, **self.docopt_options())
command = options['COMMAND'] command = options['COMMAND']
if command is None: if command is None:
raise SystemExit(command_help) raise SystemExit(getdoc(self))
handler = get_handler(self.command_class, command) handler = self.get_handler(command)
docstring = getdoc(handler) docstring = getdoc(handler)
if docstring is None: if docstring is None:
@ -37,18 +41,13 @@ class DocoptDispatcher(object):
command_options = docopt_full_help(docstring, options['ARGS'], options_first=True) command_options = docopt_full_help(docstring, options['ARGS'], options_first=True)
return options, handler, command_options return options, handler, command_options
def get_handler(self, command):
command = command.replace('-', '_')
def get_handler(command_class, command): if not hasattr(self, command):
command = command.replace('-', '_') raise NoSuchCommand(command, self)
# we certainly want to have "exec" command, since that's what docker client has
# but in python exec is a keyword
if command == "exec":
command = "exec_command"
if not hasattr(command_class, command): return getattr(self, command)
raise NoSuchCommand(command, command_class)
return getattr(command_class, command)
class NoSuchCommand(Exception): class NoSuchCommand(Exception):

View file

@ -1,30 +1,10 @@
from __future__ import absolute_import from __future__ import absolute_import
from __future__ import unicode_literals from __future__ import unicode_literals
import contextlib
import logging
import socket
from distutils.spawn import find_executable
from textwrap import dedent from textwrap import dedent
from docker.errors import APIError
from requests.exceptions import ConnectionError as RequestsConnectionError
from requests.exceptions import ReadTimeout
from requests.exceptions import SSLError
from requests.packages.urllib3.exceptions import ReadTimeoutError
from ..const import API_VERSION_TO_ENGINE_VERSION
from .utils import is_docker_for_mac_installed
from .utils import is_mac
from .utils import is_ubuntu
from .utils import is_windows
log = logging.getLogger(__name__)
class UserError(Exception): class UserError(Exception):
def __init__(self, msg): def __init__(self, msg):
self.msg = dedent(msg).strip() self.msg = dedent(msg).strip()
@ -34,104 +14,44 @@ class UserError(Exception):
__str__ = __unicode__ __str__ = __unicode__
class ConnectionError(Exception): class DockerNotFoundMac(UserError):
pass def __init__(self):
super(DockerNotFoundMac, self).__init__("""
Couldn't connect to Docker daemon. You might need to install docker-osx:
https://github.com/noplay/docker-osx
""")
@contextlib.contextmanager class DockerNotFoundUbuntu(UserError):
def handle_connection_errors(client): def __init__(self):
try: super(DockerNotFoundUbuntu, self).__init__("""
yield Couldn't connect to Docker daemon. You might need to install Docker:
except SSLError as e:
log.error('SSL error: %s' % e) https://docs.docker.com/engine/installation/ubuntulinux/
raise ConnectionError() """)
except RequestsConnectionError as e:
if e.args and isinstance(e.args[0], ReadTimeoutError):
log_timeout_error(client.timeout)
raise ConnectionError()
exit_with_error(get_conn_error_message(client.base_url))
except APIError as e:
log_api_error(e, client.api_version)
raise ConnectionError()
except (ReadTimeout, socket.timeout) as e:
log_timeout_error(client.timeout)
raise ConnectionError()
def log_timeout_error(timeout): class DockerNotFoundGeneric(UserError):
log.error( def __init__(self):
"An HTTP request took too long to complete. Retry with --verbose to " super(DockerNotFoundGeneric, self).__init__("""
"obtain debug information.\n" Couldn't connect to Docker daemon. You might need to install Docker:
"If you encounter this issue regularly because of slow network "
"conditions, consider setting COMPOSE_HTTP_TIMEOUT to a higher " https://docs.docker.com/engine/installation/
"value (current value: %s)." % timeout) """)
def log_api_error(e, client_version): class ConnectionErrorDockerMachine(UserError):
if b'client is newer than server' not in e.explanation: def __init__(self):
log.error(e.explanation) super(ConnectionErrorDockerMachine, self).__init__("""
return Couldn't connect to Docker daemon - you might need to run `docker-machine start default`.
""")
version = API_VERSION_TO_ENGINE_VERSION.get(client_version)
if not version:
# They've set a custom API version
log.error(e.explanation)
return
log.error(
"The Docker Engine version is less than the minimum required by "
"Compose. Your current project requires a Docker Engine of "
"version {version} or greater.".format(version=version))
def exit_with_error(msg): class ConnectionErrorGeneric(UserError):
log.error(dedent(msg).strip()) def __init__(self, url):
raise ConnectionError() super(ConnectionErrorGeneric, self).__init__("""
Couldn't connect to Docker daemon at %s - is it running?
If it's at a non-standard location, specify the URL with the DOCKER_HOST environment variable.
def get_conn_error_message(url): """ % url)
if find_executable('docker') is None:
return docker_not_found_msg("Couldn't connect to Docker daemon.")
if is_docker_for_mac_installed():
return conn_error_docker_for_mac
if find_executable('docker-machine') is not None:
return conn_error_docker_machine
return conn_error_generic.format(url=url)
def docker_not_found_msg(problem):
return "{} You might need to install Docker:\n\n{}".format(
problem, docker_install_url())
def docker_install_url():
if is_mac():
return docker_install_url_mac
elif is_ubuntu():
return docker_install_url_ubuntu
elif is_windows():
return docker_install_url_windows
else:
return docker_install_url_generic
docker_install_url_mac = "https://docs.docker.com/engine/installation/mac/"
docker_install_url_ubuntu = "https://docs.docker.com/engine/installation/ubuntulinux/"
docker_install_url_windows = "https://docs.docker.com/engine/installation/windows/"
docker_install_url_generic = "https://docs.docker.com/engine/installation/"
conn_error_docker_machine = """
Couldn't connect to Docker daemon - you might need to run `docker-machine start default`.
"""
conn_error_docker_for_mac = """
Couldn't connect to Docker daemon. You might need to start Docker for Mac.
"""
conn_error_generic = """
Couldn't connect to Docker daemon at {url} - is it running?
If it's at a non-standard location, specify the URL with the DOCKER_HOST environment variable.
"""

View file

@ -4,7 +4,6 @@ from __future__ import unicode_literals
import logging import logging
import os import os
import six
import texttable import texttable
from compose.cli import colors from compose.cli import colors
@ -45,7 +44,5 @@ class ConsoleWarningFormatter(logging.Formatter):
return '' return ''
def format(self, record): def format(self, record):
if isinstance(record.msg, six.binary_type):
record.msg = record.msg.decode('utf-8')
message = super(ConsoleWarningFormatter, self).format(record) message = super(ConsoleWarningFormatter, self).format(record)
return '{0}{1}'.format(self.get_level_message(record), message) return self.get_level_message(record) + message

View file

@ -2,153 +2,60 @@ from __future__ import absolute_import
from __future__ import unicode_literals from __future__ import unicode_literals
import sys import sys
from collections import namedtuple
from itertools import cycle from itertools import cycle
from threading import Thread
from docker.errors import APIError
from six.moves import _thread as thread
from six.moves.queue import Empty
from six.moves.queue import Queue
from . import colors from . import colors
from .multiplexer import Multiplexer
from compose import utils from compose import utils
from compose.cli.signals import ShutdownException
from compose.utils import split_buffer from compose.utils import split_buffer
class LogPresenter(object): class LogPrinter(object):
"""Print logs from many containers to a single output stream."""
def __init__(self, prefix_width, color_func): def __init__(self, containers, output=sys.stdout, monochrome=False, cascade_stop=False):
self.prefix_width = prefix_width self.containers = containers
self.color_func = color_func self.output = utils.get_output_stream(output)
self.monochrome = monochrome
self.cascade_stop = cascade_stop
def present(self, container, line): def run(self):
prefix = container.name_without_project.ljust(self.prefix_width) if not self.containers:
return '{prefix} {line}'.format( return
prefix=self.color_func(prefix + ' |'),
line=line) prefix_width = max_name_width(self.containers)
generators = list(self._make_log_generators(self.monochrome, prefix_width))
for line in Multiplexer(generators, cascade_stop=self.cascade_stop).loop():
self.output.write(line)
self.output.flush()
def _make_log_generators(self, monochrome, prefix_width):
def no_color(text):
return text
if monochrome:
color_funcs = cycle([no_color])
else:
color_funcs = cycle(colors.rainbow())
for color_func, container in zip(color_funcs, self.containers):
generator_func = get_log_generator(container)
prefix = color_func(build_log_prefix(container, prefix_width))
yield generator_func(container, prefix, color_func)
def build_log_presenters(service_names, monochrome): def build_log_prefix(container, prefix_width):
"""Return an iterable of functions. return container.name_without_project.ljust(prefix_width) + ' | '
Each function can be used to format the logs output of a container.
"""
prefix_width = max_name_width(service_names)
def no_color(text):
return text
for color_func in cycle([no_color] if monochrome else colors.rainbow()):
yield LogPresenter(prefix_width, color_func)
def max_name_width(service_names, max_index_width=3): def max_name_width(containers):
"""Calculate the maximum width of container names so we can make the log """Calculate the maximum width of container names so we can make the log
prefixes line up like so: prefixes line up like so:
db_1 | Listening db_1 | Listening
web_1 | Listening web_1 | Listening
""" """
return max(len(name) for name in service_names) + max_index_width return max(len(container.name_without_project) for container in containers)
class LogPrinter(object):
"""Print logs from many containers to a single output stream."""
def __init__(self,
containers,
presenters,
event_stream,
output=sys.stdout,
cascade_stop=False,
log_args=None):
self.containers = containers
self.presenters = presenters
self.event_stream = event_stream
self.output = utils.get_output_stream(output)
self.cascade_stop = cascade_stop
self.log_args = log_args or {}
def run(self):
if not self.containers:
return
queue = Queue()
thread_args = queue, self.log_args
thread_map = build_thread_map(self.containers, self.presenters, thread_args)
start_producer_thread((
thread_map,
self.event_stream,
self.presenters,
thread_args))
for line in consume_queue(queue, self.cascade_stop):
remove_stopped_threads(thread_map)
if not line:
if not thread_map:
# There are no running containers left to tail, so exit
return
# We got an empty line because of a timeout, but there are still
# active containers to tail, so continue
continue
self.output.write(line)
self.output.flush()
def remove_stopped_threads(thread_map):
for container_id, tailer_thread in list(thread_map.items()):
if not tailer_thread.is_alive():
thread_map.pop(container_id, None)
def build_thread(container, presenter, queue, log_args):
tailer = Thread(
target=tail_container_logs,
args=(container, presenter, queue, log_args))
tailer.daemon = True
tailer.start()
return tailer
def build_thread_map(initial_containers, presenters, thread_args):
return {
container.id: build_thread(container, next(presenters), *thread_args)
for container in initial_containers
}
class QueueItem(namedtuple('_QueueItem', 'item is_stop exc')):
@classmethod
def new(cls, item):
return cls(item, None, None)
@classmethod
def exception(cls, exc):
return cls(None, None, exc)
@classmethod
def stop(cls):
return cls(None, True, None)
def tail_container_logs(container, presenter, queue, log_args):
generator = get_log_generator(container)
try:
for item in generator(container, log_args):
queue.put(QueueItem.new(presenter.present(container, item)))
except Exception as e:
queue.put(QueueItem.exception(e))
return
if log_args.get('follow'):
queue.put(QueueItem.new(presenter.color_func(wait_on_exit(container))))
queue.put(QueueItem.stop())
def get_log_generator(container): def get_log_generator(container):
@ -157,81 +64,30 @@ def get_log_generator(container):
return build_no_log_generator return build_no_log_generator
def build_no_log_generator(container, log_args): def build_no_log_generator(container, prefix, color_func):
"""Return a generator that prints a warning about logs and waits for """Return a generator that prints a warning about logs and waits for
container to exit. container to exit.
""" """
yield "WARNING: no logs are available with the '{}' log driver\n".format( yield "{} WARNING: no logs are available with the '{}' log driver\n".format(
prefix,
container.log_driver) container.log_driver)
yield color_func(wait_on_exit(container))
def build_log_generator(container, log_args): def build_log_generator(container, prefix, color_func):
# if the container doesn't have a log_stream we need to attach to container # if the container doesn't have a log_stream we need to attach to container
# before log printer starts running # before log printer starts running
if container.log_stream is None: if container.log_stream is None:
stream = container.logs(stdout=True, stderr=True, stream=True, **log_args) stream = container.attach(stdout=True, stderr=True, stream=True, logs=True)
line_generator = split_buffer(stream)
else: else:
stream = container.log_stream line_generator = split_buffer(container.log_stream)
return split_buffer(stream) for line in line_generator:
yield prefix + line
yield color_func(wait_on_exit(container))
def wait_on_exit(container): def wait_on_exit(container):
try: exit_code = container.wait()
exit_code = container.wait() return "%s exited with code %s\n" % (container.name, exit_code)
return "%s exited with code %s\n" % (container.name, exit_code)
except APIError as e:
return "Unexpected API error for %s (HTTP code %s)\nResponse body:\n%s\n" % (
container.name, e.response.status_code,
e.response.text or '[empty]'
)
def start_producer_thread(thread_args):
producer = Thread(target=watch_events, args=thread_args)
producer.daemon = True
producer.start()
def watch_events(thread_map, event_stream, presenters, thread_args):
for event in event_stream:
if event['action'] == 'stop':
thread_map.pop(event['id'], None)
if event['action'] != 'start':
continue
if event['id'] in thread_map:
if thread_map[event['id']].is_alive():
continue
# Container was stopped and started, we need a new thread
thread_map.pop(event['id'], None)
thread_map[event['id']] = build_thread(
event['container'],
next(presenters),
*thread_args)
def consume_queue(queue, cascade_stop):
"""Consume the queue by reading lines off of it and yielding them."""
while True:
try:
item = queue.get(timeout=0.1)
except Empty:
yield None
continue
# See https://github.com/docker/compose/issues/189
except thread.error:
raise ShutdownException()
if item.exc:
raise item.exc
if item.is_stop:
if cascade_stop:
raise StopIteration
else:
continue
yield item.item

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,65 @@
from __future__ import absolute_import
from __future__ import unicode_literals
from threading import Thread
from six.moves import _thread as thread
try:
from Queue import Queue, Empty
except ImportError:
from queue import Queue, Empty # Python 3.x
STOP = object()
class Multiplexer(object):
"""
Create a single iterator from several iterators by running all of them in
parallel and yielding results as they come in.
"""
def __init__(self, iterators, cascade_stop=False):
self.iterators = iterators
self.cascade_stop = cascade_stop
self._num_running = len(iterators)
self.queue = Queue()
def loop(self):
self._init_readers()
while self._num_running > 0:
try:
item, exception = self.queue.get(timeout=0.1)
if exception:
raise exception
if item is STOP:
if self.cascade_stop is True:
break
else:
self._num_running -= 1
else:
yield item
except Empty:
pass
# See https://github.com/docker/compose/issues/189
except thread.error:
raise KeyboardInterrupt()
def _init_readers(self):
for iterator in self.iterators:
t = Thread(target=_enqueue_output, args=(iterator, self.queue))
t.daemon = True
t.start()
def _enqueue_output(iterator, queue):
try:
for item in iterator:
queue.put((item, None))
queue.put((STOP, None))
except Exception as e:
queue.put((None, e))

View file

@ -6,19 +6,11 @@ import os
import platform import platform
import ssl import ssl
import subprocess import subprocess
import sys
import docker import docker
from six.moves import input
import compose import compose
from ..const import IS_WINDOWS_PLATFORM
# WindowsError is not defined on non-win32 platforms. Avoid runtime errors by
# defining it as OSError (its parent class) if missing.
try:
WindowsError
except NameError:
WindowsError = OSError
def yesno(prompt, default=None): def yesno(prompt, default=None):
@ -43,16 +35,6 @@ def yesno(prompt, default=None):
return None return None
def input(prompt):
"""
Version of input (raw_input in Python 2) which forces a flush of sys.stdout
to avoid problems where the prompt fails to appear due to line buffering
"""
sys.stdout.write(prompt)
sys.stdout.flush()
return sys.stdin.readline().rstrip('\n')
def call_silently(*args, **kwargs): def call_silently(*args, **kwargs):
""" """
Like subprocess.call(), but redirects stdout and stderr to /dev/null. Like subprocess.call(), but redirects stdout and stderr to /dev/null.
@ -74,10 +56,6 @@ def is_ubuntu():
return platform.system() == 'Linux' and platform.linux_distribution()[0] == 'Ubuntu' return platform.system() == 'Linux' and platform.linux_distribution()[0] == 'Ubuntu'
def is_windows():
return IS_WINDOWS_PLATFORM
def get_version_info(scope): def get_version_info(scope):
versioninfo = 'docker-compose version {}, build {}'.format( versioninfo = 'docker-compose version {}, build {}'.format(
compose.__version__, compose.__version__,
@ -108,30 +86,3 @@ def get_build_version():
with open(filename) as fh: with open(filename) as fh:
return fh.read().strip() return fh.read().strip()
def is_docker_for_mac_installed():
return is_mac() and os.path.isdir('/Applications/Docker.app')
def generate_user_agent():
parts = [
"docker-compose/{}".format(compose.__version__),
"docker-py/{}".format(docker.__version__),
]
try:
p_system = platform.system()
p_release = platform.release()
except IOError:
pass
else:
parts.append("{}/{}".format(p_system, p_release))
return " ".join(parts)
def unquote_path(s):
if not s:
return s
if s[0] == '"' and s[-1] == '"':
return s[1:-1]
return s

View file

@ -2,7 +2,6 @@
from __future__ import absolute_import from __future__ import absolute_import
from __future__ import unicode_literals from __future__ import unicode_literals
from . import environment
from .config import ConfigurationError from .config import ConfigurationError
from .config import DOCKER_CONFIG_KEYS from .config import DOCKER_CONFIG_KEYS
from .config import find from .config import find

View file

@ -1,8 +1,10 @@
from __future__ import absolute_import from __future__ import absolute_import
from __future__ import unicode_literals from __future__ import unicode_literals
import codecs
import functools import functools
import logging import logging
import operator
import os import os
import string import string
import sys import sys
@ -12,18 +14,9 @@ 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_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 splitdrive
from .environment import env_vars_from_file
from .environment import Environment
from .environment import split_env
from .errors import CircularReference from .errors import CircularReference
from .errors import ComposeFileNotFound from .errors import ComposeFileNotFound
from .errors import ConfigurationError from .errors import ConfigurationError
@ -38,13 +31,12 @@ from .types import ServiceLink
from .types import VolumeFromSpec from .types import VolumeFromSpec
from .types import VolumeSpec from .types import VolumeSpec
from .validation import match_named_volumes from .validation import match_named_volumes
from .validation import validate_against_config_schema from .validation import validate_against_fields_schema
from .validation import validate_against_service_schema
from .validation import validate_config_section from .validation import validate_config_section
from .validation import validate_depends_on from .validation import validate_depends_on
from .validation import validate_extends_file_path from .validation import validate_extends_file_path
from .validation import validate_links
from .validation import validate_network_mode from .validation import validate_network_mode
from .validation import validate_service_constraints
from .validation import validate_top_level_object from .validation import validate_top_level_object
from .validation import validate_ulimits from .validation import validate_ulimits
@ -66,9 +58,7 @@ DOCKER_CONFIG_KEYS = [
'env_file', 'env_file',
'environment', 'environment',
'extra_hosts', 'extra_hosts',
'group_add',
'hostname', 'hostname',
'healthcheck',
'image', 'image',
'ipc', 'ipc',
'labels', 'labels',
@ -76,23 +66,17 @@ DOCKER_CONFIG_KEYS = [
'mac_address', 'mac_address',
'mem_limit', 'mem_limit',
'memswap_limit', 'memswap_limit',
'mem_swappiness',
'net', 'net',
'oom_score_adj',
'pid', 'pid',
'ports', 'ports',
'privileged', 'privileged',
'read_only', 'read_only',
'restart', 'restart',
'secrets',
'security_opt', 'security_opt',
'shm_size',
'stdin_open', 'stdin_open',
'stop_signal', 'stop_signal',
'sysctls',
'tty', 'tty',
'user', 'user',
'userns_mode',
'volume_driver', 'volume_driver',
'volumes', 'volumes',
'volumes_from', 'volumes_from',
@ -103,8 +87,6 @@ ALLOWED_KEYS = DOCKER_CONFIG_KEYS + [
'build', 'build',
'container_name', 'container_name',
'dockerfile', 'dockerfile',
'log_driver',
'log_opt',
'logging', 'logging',
'network_mode', 'network_mode',
] ]
@ -128,21 +110,13 @@ DEFAULT_OVERRIDE_FILENAME = 'docker-compose.override.yml'
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class ConfigDetails(namedtuple('_ConfigDetails', 'working_dir config_files environment')): class ConfigDetails(namedtuple('_ConfigDetails', 'working_dir config_files')):
""" """
:param working_dir: the directory to use for relative paths in the config :param working_dir: the directory to use for relative paths in the config
:type working_dir: string :type working_dir: string
:param config_files: list of configuration files to load :param config_files: list of configuration files to load
:type config_files: list of :class:`ConfigFile` :type config_files: list of :class:`ConfigFile`
:param environment: computed environment values for this project
:type environment: :class:`environment.Environment`
""" """
def __new__(cls, working_dir, config_files, environment=None):
if environment is None:
environment = Environment.from_env_file(working_dir)
return super(ConfigDetails, cls).__new__(
cls, working_dir, config_files, environment
)
class ConfigFile(namedtuple('_ConfigFile', 'filename config')): class ConfigFile(namedtuple('_ConfigFile', 'filename config')):
@ -183,8 +157,10 @@ class ConfigFile(namedtuple('_ConfigFile', 'filename config')):
if version == '2': if version == '2':
version = V2_0 version = V2_0
if version == '3': if version != V2_0:
version = V3_0 raise ConfigurationError(
'Version in "{}" is unsupported. {}'
.format(self.filename, VERSION_EXPLANATION))
return version return version
@ -200,11 +176,8 @@ 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
@ -231,13 +204,11 @@ class ServiceConfig(namedtuple('_ServiceConfig', 'working_dir filename name conf
config) config)
def find(base_dir, filenames, environment): def find(base_dir, filenames):
if filenames == ['-']: if filenames == ['-']:
return ConfigDetails( return ConfigDetails(
os.getcwd(), os.getcwd(),
[ConfigFile(None, yaml.safe_load(sys.stdin))], [ConfigFile(None, yaml.safe_load(sys.stdin))])
environment
)
if filenames: if filenames:
filenames = [os.path.join(base_dir, f) for f in filenames] filenames = [os.path.join(base_dir, f) for f in filenames]
@ -247,9 +218,7 @@ def find(base_dir, filenames, environment):
log.debug("Using configuration files: {}".format(",".join(filenames))) log.debug("Using configuration files: {}".format(",".join(filenames)))
return ConfigDetails( return ConfigDetails(
os.path.dirname(filenames[0]), os.path.dirname(filenames[0]),
[ConfigFile.from_filename(f) for f in filenames], [ConfigFile.from_filename(f) for f in filenames])
environment
)
def validate_config_version(config_files): def validate_config_version(config_files):
@ -317,7 +286,7 @@ def load(config_details):
validate_config_version(config_details.config_files) validate_config_version(config_details.config_files)
processed_files = [ processed_files = [
process_config_file(config_file, config_details.environment) process_config_file(config_file)
for config_file in config_details.config_files for config_file in config_details.config_files
] ]
config_details = config_details._replace(config_files=processed_files) config_details = config_details._replace(config_files=processed_files)
@ -329,22 +298,16 @@ 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(
service_dicts = load_services(config_details, main_file) config_details.working_dir,
main_file,
[file.get_service_dicts() for file in config_details.config_files])
if main_file.version != V1: if main_file.version != V1:
for service_dict in service_dicts: for service_dict in service_dicts:
match_named_volumes(service_dict, volumes) match_named_volumes(service_dict, volumes)
services_using_deploy = [s for s in service_dicts if s.get('deploy')] return Config(main_file.version, service_dicts, volumes, networks)
if services_using_deploy:
log.warn(
"Some services ({}) use the 'deploy' key, which will be ignored. "
"Compose does not support deploy configuration - use "
"`docker stack deploy` to deploy to a swarm."
.format(", ".join(sorted(s['name'] for s in services_using_deploy))))
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):
@ -358,66 +321,38 @@ def load_mapping(config_files, get_func, entity_type):
external = config.get('external') external = config.get('external')
if external: if external:
validate_external(entity_type, name, config) if len(config.keys()) > 1:
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']
) )
if 'labels' in config:
config['labels'] = parse_labels(config['labels'])
return mapping return mapping
def validate_external(entity_type, name, config): def load_services(working_dir, config_file, service_configs):
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 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(
config_details.working_dir, working_dir,
config_file.filename, config_file.filename,
service_name, service_name,
service_dict) service_dict)
resolver = ServiceExtendsResolver( resolver = ServiceExtendsResolver(service_config, config_file)
service_config, config_file, environment=config_details.environment
)
service_dict = process_service(resolver.run()) service_dict = process_service(resolver.run())
service_config = service_config._replace(config=service_dict) service_config = service_config._replace(config=service_dict)
@ -425,8 +360,7 @@ def load_services(config_details, config_file):
service_dict = finalize_service( service_dict = finalize_service(
service_config, service_config,
service_names, service_names,
config_file.version, config_file.version)
config_details.environment)
return service_dict return service_dict
def build_services(service_config): def build_services(service_config):
@ -446,10 +380,6 @@ def load_services(config_details, config_file):
for name in all_service_names for name in all_service_names
} }
service_configs = [
file.get_service_dicts() for file in config_details.config_files
]
service_config = service_configs[0] service_config = service_configs[0]
for next_config in service_configs[1:]: for next_config in service_configs[1:]:
service_config = merge_services(service_config, next_config) service_config = merge_services(service_config, next_config)
@ -457,45 +387,34 @@ def load_services(config_details, config_file):
return build_services(service_config) return build_services(service_config)
def interpolate_config_section(config_file, config, section, environment): def interpolate_config_section(filename, config, section):
validate_config_section(config_file.filename, config, section) validate_config_section(filename, config, section)
return interpolate_environment_variables( return interpolate_environment_variables(config, section)
config_file.version,
config,
section,
environment
)
def process_config_file(config_file, environment, service_name=None): def process_config_file(config_file, service_name=None):
services = interpolate_config_section( services = interpolate_config_section(
config_file, config_file.filename,
config_file.get_service_dicts(), config_file.get_service_dicts(),
'service', 'service')
environment)
if config_file.version in (V2_0, V2_1, V3_0, V3_1): if config_file.version == V2_0:
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(
config_file, config_file.filename,
config_file.get_volumes(), config_file.get_volumes(),
'volume', 'volume')
environment)
processed_config['networks'] = interpolate_config_section( processed_config['networks'] = interpolate_config_section(
config_file, config_file.filename,
config_file.get_networks(), config_file.get_networks(),
'network', 'network')
environment)
elif config_file.version == V1: if config_file.version == V1:
processed_config = services processed_config = services
else:
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_fields_schema(config_file)
if service_name and service_name not in services: if service_name and service_name not in services:
raise ConfigurationError( raise ConfigurationError(
@ -506,12 +425,11 @@ def process_config_file(config_file, environment, service_name=None):
class ServiceExtendsResolver(object): class ServiceExtendsResolver(object):
def __init__(self, service_config, config_file, environment, already_seen=None): def __init__(self, service_config, config_file, already_seen=None):
self.service_config = service_config self.service_config = service_config
self.working_dir = service_config.working_dir self.working_dir = service_config.working_dir
self.already_seen = already_seen or [] self.already_seen = already_seen or []
self.config_file = config_file self.config_file = config_file
self.environment = environment
@property @property
def signature(self): def signature(self):
@ -541,8 +459,8 @@ class ServiceExtendsResolver(object):
extends_file = ConfigFile.from_filename(config_path) extends_file = ConfigFile.from_filename(config_path)
validate_config_version([self.config_file, extends_file]) validate_config_version([self.config_file, extends_file])
extended_file = process_config_file( extended_file = process_config_file(
extends_file, self.environment, service_name=service_name extends_file,
) service_name=service_name)
service_config = extended_file.get_service(service_name) service_config = extended_file.get_service(service_name)
return config_path, service_config, service_name return config_path, service_config, service_name
@ -555,9 +473,7 @@ class ServiceExtendsResolver(object):
service_name, service_name,
service_dict), service_dict),
self.config_file, self.config_file,
already_seen=self.already_seen + [self.signature], already_seen=self.already_seen + [self.signature])
environment=self.environment
)
service_config = resolver.run() service_config = resolver.run()
other_service_dict = process_service(service_config) other_service_dict = process_service(service_config)
@ -586,7 +502,7 @@ class ServiceExtendsResolver(object):
return filename return filename
def resolve_environment(service_dict, environment=None): def resolve_environment(service_dict):
"""Unpack any environment variables from an env_file, if set. """Unpack any environment variables from an env_file, if set.
Interpolate environment values if set. Interpolate environment values if set.
""" """
@ -595,12 +511,12 @@ def resolve_environment(service_dict, environment=None):
env.update(env_vars_from_file(env_file)) env.update(env_vars_from_file(env_file))
env.update(parse_environment(service_dict.get('environment'))) env.update(parse_environment(service_dict.get('environment')))
return dict(resolve_env_var(k, v, environment) for k, v in six.iteritems(env)) return dict(resolve_env_var(k, v) for k, v in six.iteritems(env))
def resolve_build_args(build, environment): def resolve_build_args(build):
args = parse_build_arguments(build.get('args')) args = parse_build_arguments(build.get('args'))
return dict(resolve_env_var(k, v, environment) for k, v in six.iteritems(args)) return dict(resolve_env_var(k, v) for k, v in six.iteritems(args))
def validate_extended_service_dict(service_dict, filename, service): def validate_extended_service_dict(service_dict, filename, service):
@ -631,13 +547,12 @@ def validate_extended_service_dict(service_dict, filename, service):
def validate_service(service_config, service_names, version): def validate_service(service_config, service_names, version):
service_dict, service_name = service_config.config, service_config.name service_dict, service_name = service_config.config, service_config.name
validate_service_constraints(service_dict, service_name, version) validate_against_service_schema(service_dict, service_name, version)
validate_paths(service_dict) validate_paths(service_dict)
validate_ulimits(service_config) validate_ulimits(service_config)
validate_network_mode(service_config, service_names) validate_network_mode(service_config, service_names)
validate_depends_on(service_config, service_names) validate_depends_on(service_config, service_names)
validate_links(service_config, service_names)
if not service_dict.get('image') and has_uppercase(service_name): if not service_dict.get('image') and has_uppercase(service_name):
raise ConfigurationError( raise ConfigurationError(
@ -673,67 +588,18 @@ def process_service(service_config):
if 'extra_hosts' in service_dict: if 'extra_hosts' in service_dict:
service_dict['extra_hosts'] = parse_extra_hosts(service_dict['extra_hosts']) service_dict['extra_hosts'] = parse_extra_hosts(service_dict['extra_hosts'])
if 'sysctls' in service_dict: for field in ['dns', 'dns_search']:
service_dict['sysctls'] = build_string_dict(parse_sysctls(service_dict['sysctls']))
service_dict = process_depends_on(service_dict)
for field in ['dns', 'dns_search', 'tmpfs']:
if field in service_dict: if field in service_dict:
service_dict[field] = to_list(service_dict[field]) service_dict[field] = to_list(service_dict[field])
service_dict = process_healthcheck(service_dict, service_config.name)
return service_dict return service_dict
def process_depends_on(service_dict): def finalize_service(service_config, service_names, version):
if 'depends_on' in service_dict and not isinstance(service_dict['depends_on'], dict):
service_dict['depends_on'] = dict([
(svc, {'condition': 'service_started'}) for svc in service_dict['depends_on']
])
return service_dict
def process_healthcheck(service_dict, service_name):
if 'healthcheck' not in service_dict:
return service_dict
hc = {}
raw = service_dict['healthcheck']
if raw.get('disable'):
if len(raw) > 1:
raise ConfigurationError(
'Service "{}" defines an invalid healthcheck: '
'"disable: true" cannot be combined with other options'
.format(service_name))
hc['test'] = ['NONE']
elif 'test' in raw:
hc['test'] = raw['test']
if 'interval' in raw:
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 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:
hc['retries'] = raw['retries']
service_dict['healthcheck'] = hc
return service_dict
def finalize_service(service_config, service_names, version, environment):
service_dict = dict(service_config.config) service_dict = dict(service_config.config)
if 'environment' in service_dict or 'env_file' in service_dict: if 'environment' in service_dict or 'env_file' in service_dict:
service_dict['environment'] = resolve_environment(service_dict, environment) service_dict['environment'] = resolve_environment(service_dict)
service_dict.pop('env_file', None) service_dict.pop('env_file', None)
if 'volumes_from' in service_dict: if 'volumes_from' in service_dict:
@ -744,10 +610,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) for v in service_dict['volumes']]
v, environment.get_boolean('COMPOSE_CONVERT_WINDOWS_PATHS')
) for v in service_dict['volumes']
]
if 'net' in service_dict: if 'net' in service_dict:
network_mode = service_dict.pop('net') network_mode = service_dict.pop('net')
@ -763,12 +626,7 @@ 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: normalize_build(service_dict, service_config.working_dir)
service_dict['secrets'] = [
types.ServiceSecret.parse(s) for s in service_dict['secrets']
]
normalize_build(service_dict, service_config.working_dir, environment)
service_dict['name'] = service_config.name service_dict['name'] = service_config.name
return normalize_v1_service_format(service_dict) return normalize_v1_service_format(service_dict)
@ -841,7 +699,7 @@ class MergeDict(dict):
merged = parse_sequence_func(self.base.get(field, [])) merged = parse_sequence_func(self.base.get(field, []))
merged.update(parse_sequence_func(self.override.get(field, []))) merged.update(parse_sequence_func(self.override.get(field, [])))
self[field] = [item.repr() for item in sorted(merged.values())] self[field] = [item.repr() for item in merged.values()]
def merge_scalar(self, field): def merge_scalar(self, field):
if self.needs_merge(field): if self.needs_merge(field):
@ -855,25 +713,23 @@ def merge_service_dicts(base, override, version):
md.merge_mapping('labels', parse_labels) md.merge_mapping('labels', parse_labels)
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('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', 'depends_on',
'security_opt', 'volumes_from', 'expose',
'external_links',
'ports',
'volumes_from',
]: ]:
md.merge_field(field, merge_unique_items_lists, default=[]) md.merge_field(field, operator.add, default=[])
for field in ['dns', 'dns_search', 'env_file', 'tmpfs']: for field in ['dns', 'dns_search', 'env_file']:
md.merge_field(field, merge_list_or_string) md.merge_field(field, merge_list_or_string)
md.merge_field('logging', merge_logging, default={})
for field in set(ALLOWED_KEYS) - set(md): for field in set(ALLOWED_KEYS) - set(md):
md.merge_scalar(field) md.merge_scalar(field)
@ -885,10 +741,6 @@ def merge_service_dicts(base, override, version):
return dict(md) return dict(md)
def merge_unique_items_lists(base, override):
return sorted(set().union(base, override))
def merge_build(output, base, override): def merge_build(output, base, override):
def to_dict(service): def to_dict(service):
build_config = service.get('build', {}) build_config = service.get('build', {})
@ -903,16 +755,6 @@ def merge_build(output, base, override):
return dict(md) return dict(md)
def merge_logging(base, override):
md = MergeDict(base, override)
md.merge_scalar('driver')
if md.get('driver') == base.get('driver') or base.get('driver') is None:
md.merge_mapping('options', lambda m: m or {})
else:
md['options'] = override.get('options')
return dict(md)
def legacy_v1_merge_image_or_build(output, base, override): def legacy_v1_merge_image_or_build(output, base, override):
output.pop('image', None) output.pop('image', None)
output.pop('build', None) output.pop('build', None)
@ -932,11 +774,20 @@ def merge_environment(base, override):
return env return env
def split_kv(kvpair): def split_env(env):
if '=' in kvpair: if isinstance(env, six.binary_type):
return kvpair.split('=', 1) env = env.decode('utf-8', 'replace')
if '=' in env:
return env.split('=', 1)
else: else:
return kvpair, '' return env, None
def split_label(label):
if '=' in label:
return label.split('=', 1)
else:
return label, ''
def parse_dict_or_list(split_func, type_name, arguments): def parse_dict_or_list(split_func, type_name, arguments):
@ -957,12 +808,8 @@ def parse_dict_or_list(split_func, type_name, arguments):
parse_build_arguments = functools.partial(parse_dict_or_list, split_env, 'build arguments') parse_build_arguments = functools.partial(parse_dict_or_list, split_env, 'build arguments')
parse_environment = functools.partial(parse_dict_or_list, split_env, 'environment') parse_environment = functools.partial(parse_dict_or_list, split_env, 'environment')
parse_labels = functools.partial(parse_dict_or_list, split_kv, 'labels') parse_labels = functools.partial(parse_dict_or_list, split_label, '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_depends_on = functools.partial(
parse_dict_or_list, lambda k: (k, {'condition': 'service_started'}), 'depends_on'
)
def parse_ulimits(ulimits): def parse_ulimits(ulimits):
@ -973,15 +820,30 @@ def parse_ulimits(ulimits):
return dict(ulimits) return dict(ulimits)
def resolve_env_var(key, val, environment): def resolve_env_var(key, val):
if val is not None: if val is not None:
return key, val return key, val
elif environment and key in environment: elif key in os.environ:
return key, environment[key] return key, os.environ[key]
else: else:
return key, None return key, None
def env_vars_from_file(filename):
"""
Read in a line delimited file of environment variables.
"""
if not os.path.exists(filename):
raise ConfigurationError("Couldn't find env file: %s" % filename)
env = {}
for line in codecs.open(filename, 'r', 'utf-8'):
line = line.strip()
if line and not line.startswith('#'):
k, v = split_env(line)
env[k] = v
return env
def resolve_volume_paths(working_dir, service_dict): def resolve_volume_paths(working_dir, service_dict):
return [ return [
resolve_volume_path(working_dir, volume) resolve_volume_path(working_dir, volume)
@ -1001,7 +863,7 @@ def resolve_volume_path(working_dir, volume):
return container_path return container_path
def normalize_build(service_dict, working_dir, environment): def normalize_build(service_dict, working_dir):
if 'build' in service_dict: if 'build' in service_dict:
build = {} build = {}
@ -1011,9 +873,7 @@ def normalize_build(service_dict, working_dir, environment):
else: else:
build.update(service_dict['build']) build.update(service_dict['build'])
if 'args' in build: if 'args' in build:
build['args'] = build_string_dict( build['args'] = build_string_dict(resolve_build_args(build))
resolve_build_args(build, environment)
)
service_dict['build'] = build service_dict['build'] = build
@ -1063,7 +923,7 @@ def dict_from_path_mappings(path_mappings):
def path_mappings_from_dict(d): def path_mappings_from_dict(d):
return [join_path_mapping(v) for v in sorted(d.items())] return [join_path_mapping(v) for v in d.items()]
def split_path_mapping(volume_path): def split_path_mapping(volume_path):
@ -1072,7 +932,12 @@ def split_path_mapping(volume_path):
path. Using splitdrive so windows absolute paths won't cause issues with path. Using splitdrive so windows absolute paths won't cause issues with
splitting on ':'. splitting on ':'.
""" """
drive, volume_config = splitdrive(volume_path) # splitdrive has limitations when it comes to relative paths, so when it's
# relative, handle special case to set the drive to ''
if volume_path.startswith('.') or volume_path.startswith('~'):
drive, volume_config = '', volume_path
else:
drive, volume_config = os.path.splitdrive(volume_path)
if ':' in volume_config: if ':' in volume_config:
(host, container) = volume_config.split(':', 1) (host, container) = volume_config.split(':', 1)

View file

@ -1,376 +0,0 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "config_schema_v2.1.json",
"type": "object",
"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
}
},
"additionalProperties": false,
"definitions": {
"service": {
"id": "#/definitions/service",
"type": "object",
"properties": {
"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"},
"cpu_shares": {"type": ["number", "string"]},
"cpu_quota": {"type": ["number", "string"]},
"cpuset": {"type": "string"},
"depends_on": {
"oneOf": [
{"$ref": "#/definitions/list_of_strings"},
{
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"type": "object",
"additionalProperties": false,
"properties": {
"condition": {
"type": "string",
"enum": ["service_started", "service_healthy"]
}
},
"required": ["condition"]
}
}
}
]
},
"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
},
"extends": {
"oneOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"service": {"type": "string"},
"file": {"type": "string"}
},
"required": ["service"],
"additionalProperties": false
}
]
},
"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"},
"isolation": {"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"}
},
"additionalProperties": false
},
"mac_address": {"type": "string"},
"mem_limit": {"type": ["number", "string"]},
"memswap_limit": {"type": ["number", "string"]},
"mem_swappiness": {"type": "integer"},
"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"},
"link_local_ips": {"$ref": "#/definitions/list_of_strings"}
},
"additionalProperties": false
},
{"type": "null"}
]
}
},
"additionalProperties": false
}
]
},
"oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000},
"group_add": {
"type": "array",
"items": {
"type": ["string", "number"]
},
"uniqueItems": true
},
"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"]},
"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},
"volume_driver": {"type": "string"},
"volumes_from": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"working_dir": {"type": "string"}
},
"dependencies": {
"memswap_limit": ["mem_limit"]
},
"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"}
}
},
"network": {
"id": "#/definitions/network",
"type": "object",
"properties": {
"driver": {"type": "string"},
"driver_opts": {
"type": "object",
"patternProperties": {
"^.+$": {"type": ["string", "number"]}
}
},
"ipam": {
"type": "object",
"properties": {
"driver": {"type": "string"},
"config": {
"type": "array"
}
},
"additionalProperties": false
},
"external": {
"type": ["boolean", "object"],
"properties": {
"name": {"type": "string"}
},
"additionalProperties": false
},
"internal": {"type": "boolean"},
"enable_ipv6": {"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
},
"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

@ -1,383 +0,0 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "config_schema_v3.0.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
}
},
"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"]},
"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
},
"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

@ -1,428 +0,0 @@
{
"$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

@ -1,120 +0,0 @@
from __future__ import absolute_import
from __future__ import unicode_literals
import codecs
import contextlib
import logging
import os
import six
from ..const import IS_WINDOWS_PLATFORM
from .errors import ConfigurationError
log = logging.getLogger(__name__)
def split_env(env):
if isinstance(env, six.binary_type):
env = env.decode('utf-8', 'replace')
if '=' in env:
return env.split('=', 1)
else:
return env, None
def env_vars_from_file(filename):
"""
Read in a line delimited file of environment variables.
"""
if not os.path.exists(filename):
raise ConfigurationError("Couldn't find env file: %s" % filename)
elif not os.path.isfile(filename):
raise ConfigurationError("%s is not a file." % (filename))
env = {}
with contextlib.closing(codecs.open(filename, 'r', 'utf-8')) as fileobj:
for line in fileobj:
line = line.strip()
if line and not line.startswith('#'):
k, v = split_env(line)
env[k] = v
return env
class Environment(dict):
def __init__(self, *args, **kwargs):
super(Environment, self).__init__(*args, **kwargs)
self.missing_keys = []
@classmethod
def from_env_file(cls, base_dir):
def _initialize():
result = cls()
if base_dir is None:
return result
env_file_path = os.path.join(base_dir, '.env')
try:
return cls(env_vars_from_file(env_file_path))
except ConfigurationError:
pass
return result
instance = _initialize()
instance.update(os.environ)
return instance
@classmethod
def from_command_line(cls, parsed_env_opts):
result = cls()
for k, v in parsed_env_opts.items():
# Values from the command line take priority, unless they're unset
# in which case they take the value from the system's environment
if v is None and k in os.environ:
result[k] = os.environ[k]
else:
result[k] = v
return result
def __getitem__(self, key):
try:
return super(Environment, self).__getitem__(key)
except KeyError:
if IS_WINDOWS_PLATFORM:
try:
return super(Environment, self).__getitem__(key.upper())
except KeyError:
pass
if key not in self.missing_keys:
log.warn(
"The {} variable is not set. Defaulting to a blank string."
.format(key)
)
self.missing_keys.append(key)
return ""
def __contains__(self, key):
result = super(Environment, self).__contains__(key)
if IS_WINDOWS_PLATFORM:
return (
result or super(Environment, self).__contains__(key.upper())
)
return result
def get(self, key, *args, **kwargs):
if IS_WINDOWS_PLATFORM:
return super(Environment, self).get(
key,
super(Environment, self).get(key.upper(), *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

@ -3,11 +3,10 @@ from __future__ import unicode_literals
VERSION_EXPLANATION = ( VERSION_EXPLANATION = (
'You might be seeing this error because you\'re using the wrong Compose file version. ' 'Either specify a version of "2" (or "2.0") and place your service '
'Either specify a supported version ("2.0", "2.1", "3.0") and place your ' 'definitions under the `services` key, or omit the `version` key and place '
'service definitions under the `services` key, or omit the `version` key ' 'your service definitions at the root of the file to use version 1.\n'
'and place your service definitions at the root of the file to use ' 'For more on the Compose file format versions, see '
'version 1.\nFor more on the Compose file format versions, see '
'https://docs.docker.com/compose/compose-file/') 'https://docs.docker.com/compose/compose-file/')
@ -39,8 +38,7 @@ class CircularReference(ConfigurationError):
class ComposeFileNotFound(ConfigurationError): class ComposeFileNotFound(ConfigurationError):
def __init__(self, supported_filenames): def __init__(self, supported_filenames):
super(ComposeFileNotFound, self).__init__(""" super(ComposeFileNotFound, self).__init__("""
Can't find a suitable configuration file in this directory or any Can't find a suitable configuration file in this directory or any parent. Are you in the right directory?
parent. Are you in the right directory?
Supported filenames: %s Supported filenames: %s
""" % ", ".join(supported_filenames)) """ % ", ".join(supported_filenames))

View file

@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"id": "fields_schema_v1.json",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "service_schema_v1.json#/definitions/service"
}
},
"additionalProperties": false
}

View file

@ -0,0 +1,96 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"id": "fields_schema_v2.0.json",
"properties": {
"version": {
"type": "string"
},
"services": {
"id": "#/properties/services",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "service_schema_v2.0.json#/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
}
},
"definitions": {
"network": {
"id": "#/definitions/network",
"type": "object",
"properties": {
"driver": {"type": "string"},
"driver_opts": {
"type": "object",
"patternProperties": {
"^.+$": {"type": ["string", "number"]}
}
},
"ipam": {
"type": "object",
"properties": {
"driver": {"type": "string"},
"config": {
"type": "array"
}
},
"additionalProperties": false
},
"external": {
"type": ["boolean", "object"],
"properties": {
"name": {"type": "string"}
},
"additionalProperties": false
}
},
"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
},
"additionalProperties": false
}
},
"additionalProperties": false
}

View file

@ -2,40 +2,21 @@ from __future__ import absolute_import
from __future__ import unicode_literals from __future__ import unicode_literals
import logging import logging
import os
from string import Template from string import Template
import six import six
from .errors import ConfigurationError from .errors import ConfigurationError
from compose.const import COMPOSEFILE_V1 as V1
from compose.const import COMPOSEFILE_V2_0 as V2_0
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class Interpolator(object): def interpolate_environment_variables(config, section):
mapping = BlankDefaultDict(os.environ)
def __init__(self, templater, mapping):
self.templater = templater
self.mapping = mapping
def interpolate(self, string):
try:
return self.templater(string).substitute(self.mapping)
except ValueError:
raise InvalidInterpolation(string)
def interpolate_environment_variables(version, config, section, environment):
if version in (V2_0, V1):
interpolator = Interpolator(Template, environment)
else:
interpolator = Interpolator(TemplateWithDefaults, environment)
def process_item(name, config_dict): def process_item(name, config_dict):
return dict( return dict(
(key, interpolate_value(name, key, val, section, interpolator)) (key, interpolate_value(name, key, val, section, mapping))
for key, val in (config_dict or {}).items() for key, val in (config_dict or {}).items()
) )
@ -45,9 +26,9 @@ def interpolate_environment_variables(version, config, section, environment):
) )
def interpolate_value(name, config_key, value, section, interpolator): def interpolate_value(name, config_key, value, section, mapping):
try: try:
return recursive_interpolate(value, interpolator) return recursive_interpolate(value, mapping)
except InvalidInterpolation as e: except InvalidInterpolation as e:
raise ConfigurationError( raise ConfigurationError(
'Invalid interpolation format for "{config_key}" option ' 'Invalid interpolation format for "{config_key}" option '
@ -58,44 +39,44 @@ def interpolate_value(name, config_key, value, section, interpolator):
string=e.string)) string=e.string))
def recursive_interpolate(obj, interpolator): def recursive_interpolate(obj, mapping):
if isinstance(obj, six.string_types): if isinstance(obj, six.string_types):
return interpolator.interpolate(obj) return interpolate(obj, mapping)
if isinstance(obj, dict): elif isinstance(obj, dict):
return dict( return dict(
(key, recursive_interpolate(val, interpolator)) (key, recursive_interpolate(val, mapping))
for (key, val) in obj.items() for (key, val) in obj.items()
) )
if isinstance(obj, list): elif isinstance(obj, list):
return [recursive_interpolate(val, interpolator) for val in obj] return [recursive_interpolate(val, mapping) for val in obj]
return obj else:
return obj
class TemplateWithDefaults(Template): def interpolate(string, mapping):
idpattern = r'[_a-z][_a-z0-9]*(?::?-[^}]+)?' try:
return Template(string).substitute(mapping)
except ValueError:
raise InvalidInterpolation(string)
# Modified from python2.7/string.py
def substitute(self, mapping): class BlankDefaultDict(dict):
# Helper function for .sub() def __init__(self, *args, **kwargs):
def convert(mo): super(BlankDefaultDict, self).__init__(*args, **kwargs)
# Check the most common path first. self.missing_keys = []
named = mo.group('named') or mo.group('braced')
if named is not None: def __getitem__(self, key):
if ':-' in named: try:
var, _, default = named.partition(':-') return super(BlankDefaultDict, self).__getitem__(key)
return mapping.get(var) or default except KeyError:
if '-' in named: if key not in self.missing_keys:
var, _, default = named.partition('-') log.warn(
return mapping.get(var, default) "The {} variable is not set. Defaulting to a blank string."
val = mapping[named] .format(key)
return '%s' % (val,) )
if mo.group('escaped') is not None: self.missing_keys.append(key)
return self.delimiter
if mo.group('invalid') is not None: return ""
self._invalid(mo)
raise ValueError('Unrecognized named group in pattern',
self.pattern)
return self.pattern.sub(convert, self.template)
class InvalidInterpolation(Exception): class InvalidInterpolation(Exception):

View file

@ -5,8 +5,6 @@ import six
import yaml import yaml
from compose.config import types from compose.config import types
from compose.config.config import V1
from compose.config.config import V2_1
def serialize_config_type(dumper, data): def serialize_config_type(dumper, data):
@ -18,91 +16,15 @@ yaml.SafeDumper.add_representer(types.VolumeFromSpec, serialize_config_type)
yaml.SafeDumper.add_representer(types.VolumeSpec, serialize_config_type) yaml.SafeDumper.add_representer(types.VolumeSpec, serialize_config_type)
def denormalize_config(config):
denormalized_services = [
denormalize_service_dict(service_dict, config.version)
for service_dict in config.services
]
services = {
service_dict.pop('name'): service_dict
for service_dict in denormalized_services
}
networks = config.networks.copy()
for net_name, net_conf in networks.items():
if 'external_name' in net_conf:
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
if version == V1:
version = V2_1
return {
'version': version,
'services': services,
'networks': networks,
'volumes': volumes,
}
def serialize_config(config): def serialize_config(config):
output = {
'version': config.version,
'services': {service.pop('name'): service for service in config.services},
'networks': config.networks,
'volumes': config.volumes,
}
return yaml.safe_dump( return yaml.safe_dump(
denormalize_config(config), output,
default_flow_style=False, default_flow_style=False,
indent=2, indent=2,
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):
service_dict = service_dict.copy()
if 'restart' in service_dict:
service_dict['restart'] = types.serialize_restart_spec(
service_dict['restart']
)
if version == V1 and 'network_mode' not in service_dict:
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

View file

@ -1,16 +1,13 @@
{ {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"id": "config_schema_v1.json", "id": "service_schema_v1.json",
"type": "object", "type": "object",
"patternProperties": { "allOf": [
"^[a-zA-Z0-9._-]+$": { {"$ref": "#/definitions/service"},
"$ref": "#/definitions/service" {"$ref": "#/definitions/constraints"}
} ],
},
"additionalProperties": false,
"definitions": { "definitions": {
"service": { "service": {
@ -85,7 +82,6 @@
"mac_address": {"type": "string"}, "mac_address": {"type": "string"},
"mem_limit": {"type": ["number", "string"]}, "mem_limit": {"type": ["number", "string"]},
"memswap_limit": {"type": ["number", "string"]}, "memswap_limit": {"type": ["number", "string"]},
"mem_swappiness": {"type": "integer"},
"net": {"type": "string"}, "net": {"type": "string"},
"pid": {"type": ["string", "null"]}, "pid": {"type": ["string", "null"]},
@ -102,7 +98,6 @@
"read_only": {"type": "boolean"}, "read_only": {"type": "boolean"},
"restart": {"type": "string"}, "restart": {"type": "string"},
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"shm_size": {"type": ["number", "string"]},
"stdin_open": {"type": "boolean"}, "stdin_open": {"type": "boolean"},
"stop_signal": {"type": "string"}, "stop_signal": {"type": "string"},
"tty": {"type": "boolean"}, "tty": {"type": "boolean"},
@ -157,7 +152,8 @@
"type": "object", "type": "object",
"patternProperties": { "patternProperties": {
".+": { ".+": {
"type": ["string", "number", "null"] "type": ["string", "number", "boolean", "null"],
"format": "bool-value-in-mapping"
} }
}, },
"additionalProperties": false "additionalProperties": false
@ -165,24 +161,21 @@
{"type": "array", "items": {"type": "string"}, "uniqueItems": true} {"type": "array", "items": {"type": "string"}, "uniqueItems": true}
] ]
}, },
"constraints": { "constraints": {
"service": { "id": "#/definitions/constraints",
"id": "#/definitions/constraints/service", "anyOf": [
"anyOf": [ {
{ "required": ["build"],
"required": ["build"], "not": {"required": ["image"]}
"not": {"required": ["image"]} },
}, {
{ "required": ["image"],
"required": ["image"], "not": {"anyOf": [
"not": {"anyOf": [ {"required": ["build"]},
{"required": ["build"]}, {"required": ["dockerfile"]}
{"required": ["dockerfile"]} ]}
]} }
} ]
]
}
} }
} }
} }

View file

@ -1,50 +1,15 @@
{ {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"id": "config_schema_v2.0.json", "id": "service_schema_v2.0.json",
"type": "object", "type": "object",
"properties": { "allOf": [
"version": { {"$ref": "#/definitions/service"},
"type": "string" {"$ref": "#/definitions/constraints"}
}, ],
"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
}
},
"additionalProperties": false,
"definitions": { "definitions": {
"service": { "service": {
"id": "#/definitions/service", "id": "#/definitions/service",
"type": "object", "type": "object",
@ -58,7 +23,20 @@
"properties": { "properties": {
"context": {"type": "string"}, "context": {"type": "string"},
"dockerfile": {"type": "string"}, "dockerfile": {"type": "string"},
"args": {"$ref": "#/definitions/list_or_dict"} "args": {
"oneOf": [
{"$ref": "#/definitions/list_of_strings"},
{
"type": "object",
"patternProperties": {
"^.+$": {
"type": ["string", "number"]
}
},
"additionalProperties": false
}
]
}
}, },
"additionalProperties": false "additionalProperties": false
} }
@ -139,7 +117,6 @@
"mac_address": {"type": "string"}, "mac_address": {"type": "string"},
"mem_limit": {"type": ["number", "string"]}, "mem_limit": {"type": ["number", "string"]},
"memswap_limit": {"type": ["number", "string"]}, "memswap_limit": {"type": ["number", "string"]},
"mem_swappiness": {"type": "integer"},
"network_mode": {"type": "string"}, "network_mode": {"type": "string"},
"networks": { "networks": {
@ -153,9 +130,7 @@
{ {
"type": "object", "type": "object",
"properties": { "properties": {
"aliases": {"$ref": "#/definitions/list_of_strings"}, "aliases": {"$ref": "#/definitions/list_of_strings"}
"ipv4_address": {"type": "string"},
"ipv6_address": {"type": "string"}
}, },
"additionalProperties": false "additionalProperties": false
}, },
@ -167,14 +142,6 @@
} }
] ]
}, },
"oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000},
"group_add": {
"type": "array",
"items": {
"type": ["string", "number"]
},
"uniqueItems": true
},
"pid": {"type": ["string", "null"]}, "pid": {"type": ["string", "null"]},
"ports": { "ports": {
@ -190,11 +157,8 @@
"read_only": {"type": "boolean"}, "read_only": {"type": "boolean"},
"restart": {"type": "string"}, "restart": {"type": "string"},
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"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"},
"tty": {"type": "boolean"}, "tty": {"type": "boolean"},
"ulimits": { "ulimits": {
"type": "object", "type": "object",
@ -228,61 +192,6 @@
"additionalProperties": false "additionalProperties": false
}, },
"network": {
"id": "#/definitions/network",
"type": "object",
"properties": {
"driver": {"type": "string"},
"driver_opts": {
"type": "object",
"patternProperties": {
"^.+$": {"type": ["string", "number"]}
}
},
"ipam": {
"type": "object",
"properties": {
"driver": {"type": "string"},
"config": {
"type": "array"
}
},
"additionalProperties": false
},
"external": {
"type": ["boolean", "object"],
"properties": {
"name": {"type": "string"}
},
"additionalProperties": false
},
"internal": {"type": "boolean"}
},
"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
}
},
"additionalProperties": false
},
"string_or_list": { "string_or_list": {
"oneOf": [ "oneOf": [
{"type": "string"}, {"type": "string"},
@ -302,7 +211,8 @@
"type": "object", "type": "object",
"patternProperties": { "patternProperties": {
".+": { ".+": {
"type": ["string", "number", "null"] "type": ["string", "number", "boolean", "null"],
"format": "bool-value-in-mapping"
} }
}, },
"additionalProperties": false "additionalProperties": false
@ -310,18 +220,15 @@
{"type": "array", "items": {"type": "string"}, "uniqueItems": true} {"type": "array", "items": {"type": "string"}, "uniqueItems": true}
] ]
}, },
"constraints": { "constraints": {
"service": { "id": "#/definitions/constraints",
"id": "#/definitions/constraints/service", "anyOf": [
"anyOf": [
{"required": ["build"]}, {"required": ["build"]},
{"required": ["image"]} {"required": ["image"]}
], ],
"properties": { "properties": {
"build": { "build": {
"required": ["context"] "required": ["context"]
}
} }
} }
} }

View file

@ -23,31 +23,28 @@ def get_source_name_from_network_mode(network_mode, source_type):
return net_name return net_name
def get_service_names(links):
return [link.split(':')[0] for link in links]
def get_service_names_from_volumes_from(volumes_from):
return [volume_from.source for volume_from in volumes_from]
def get_service_dependents(service_dict, services):
name = service_dict['name']
return [
service for service in services
if (name in get_service_names(service.get('links', [])) or
name in get_service_names_from_volumes_from(service.get('volumes_from', [])) or
name == get_service_name_from_network_mode(service.get('network_mode')) or
name in service.get('depends_on', []))
]
def sort_service_dicts(services): def sort_service_dicts(services):
# Topological sort (Cormen/Tarjan algorithm). # Topological sort (Cormen/Tarjan algorithm).
unmarked = services[:] unmarked = services[:]
temporary_marked = set() temporary_marked = set()
sorted_services = [] sorted_services = []
def get_service_names(links):
return [link.split(':')[0] for link in links]
def get_service_names_from_volumes_from(volumes_from):
return [volume_from.source for volume_from in volumes_from]
def get_service_dependents(service_dict, services):
name = service_dict['name']
return [
service for service in services
if (name in get_service_names(service.get('links', [])) or
name in get_service_names_from_volumes_from(service.get('volumes_from', [])) or
name == get_service_name_from_network_mode(service.get('network_mode')) or
name in service.get('depends_on', []))
]
def visit(n): def visit(n):
if n['name'] in temporary_marked: if n['name'] in temporary_marked:
if n['name'] in get_service_names(n.get('links', [])): if n['name'] in get_service_names(n.get('links', [])):

View file

@ -5,17 +5,11 @@ from __future__ import absolute_import
from __future__ import unicode_literals from __future__ import unicode_literals
import os import os
import re
from collections import namedtuple from collections import namedtuple
import six from compose.config.config import V1
from compose.config.errors import ConfigurationError
from ..const import COMPOSEFILE_V1 as V1
from .errors import ConfigurationError
from compose.const import IS_WINDOWS_PLATFORM from compose.const import IS_WINDOWS_PLATFORM
from compose.utils import splitdrive
win32_root_path_pattern = re.compile(r'^[A-Za-z]\:\\.*')
class VolumeFromSpec(namedtuple('_VolumeFromSpec', 'source mode type')): class VolumeFromSpec(namedtuple('_VolumeFromSpec', 'source mode type')):
@ -95,15 +89,6 @@ def parse_restart_spec(restart_config):
return {'Name': name, 'MaximumRetryCount': int(max_retry_count)} return {'Name': name, 'MaximumRetryCount': int(max_retry_count)}
def serialize_restart_spec(restart_spec):
if not restart_spec:
return ''
parts = [restart_spec['Name']]
if restart_spec['MaximumRetryCount']:
parts.append(six.text_type(restart_spec['MaximumRetryCount']))
return ':'.join(parts)
def parse_extra_hosts(extra_hosts_config): def parse_extra_hosts(extra_hosts_config):
if not extra_hosts_config: if not extra_hosts_config:
return {} return {}
@ -120,23 +105,41 @@ def parse_extra_hosts(extra_hosts_config):
return extra_hosts_dict return extra_hosts_dict
def normalize_path_for_engine(path): def normalize_paths_for_engine(external_path, internal_path):
"""Windows paths, c:\my\path\shiny, need to be changed to be compatible with """Windows paths, c:\my\path\shiny, need to be changed to be compatible with
the Engine. Volume paths are expected to be linux style /c/my/path/shiny/ the Engine. Volume paths are expected to be linux style /c/my/path/shiny/
""" """
drive, tail = splitdrive(path) if not IS_WINDOWS_PLATFORM:
return external_path, internal_path
if drive: if external_path:
path = '/' + drive.lower().rstrip(':') + tail drive, tail = os.path.splitdrive(external_path)
return path.replace('\\', '/') if drive:
external_path = '/' + drive.lower().rstrip(':') + tail
external_path = external_path.replace('\\', '/')
return external_path, internal_path.replace('\\', '/')
class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')): class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')):
@classmethod @classmethod
def _parse_unix(cls, volume_config): def parse(cls, volume_config):
parts = volume_config.split(':') """Parse a volume_config path and split it into external:internal[:mode]
parts to be returned as a valid VolumeSpec.
"""
if IS_WINDOWS_PLATFORM:
# relative paths in windows expand to include the drive, eg C:\
# so we join the first 2 parts back together to count as one
drive, tail = os.path.splitdrive(volume_config)
parts = tail.split(":")
if drive:
parts[0] = drive + parts[0]
else:
parts = volume_config.split(':')
if len(parts) > 3: if len(parts) > 3:
raise ConfigurationError( raise ConfigurationError(
@ -144,11 +147,13 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')):
"external:internal[:mode]" % volume_config) "external:internal[:mode]" % volume_config)
if len(parts) == 1: if len(parts) == 1:
external = None external, internal = normalize_paths_for_engine(
internal = os.path.normpath(parts[0]) None,
os.path.normpath(parts[0]))
else: else:
external = os.path.normpath(parts[0]) external, internal = normalize_paths_for_engine(
internal = os.path.normpath(parts[1]) os.path.normpath(parts[0]),
os.path.normpath(parts[1]))
mode = 'rw' mode = 'rw'
if len(parts) == 3: if len(parts) == 3:
@ -156,65 +161,13 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')):
return cls(external, internal, mode) return cls(external, internal, mode)
@classmethod
def _parse_win32(cls, volume_config, normalize):
# relative paths in windows expand to include the drive, eg C:\
# so we join the first 2 parts back together to count as one
mode = 'rw'
def separate_next_section(volume_config):
drive, tail = splitdrive(volume_config)
parts = tail.split(':', 1)
if drive:
parts[0] = drive + parts[0]
return parts
parts = separate_next_section(volume_config)
if len(parts) == 1:
internal = parts[0]
external = None
else:
external = parts[0]
parts = separate_next_section(parts[1])
external = os.path.normpath(external)
internal = parts[0]
if len(parts) > 1:
if ':' in parts[1]:
raise ConfigurationError(
"Volume %s has incorrect format, should be "
"external:internal[:mode]" % volume_config
)
mode = parts[1]
if normalize:
external = normalize_path_for_engine(external) if external else None
return cls(external, internal, mode)
@classmethod
def parse(cls, volume_config, normalize=False):
"""Parse a volume_config path and split it into external:internal[:mode]
parts to be returned as a valid VolumeSpec.
"""
if IS_WINDOWS_PLATFORM:
return cls._parse_win32(volume_config, normalize)
else:
return cls._parse_unix(volume_config)
def repr(self): def repr(self):
external = self.external + ':' if self.external else '' external = self.external + ':' if self.external else ''
return '{ext}{v.internal}:{v.mode}'.format(ext=external, v=self) return '{ext}{v.internal}:{v.mode}'.format(ext=external, v=self)
@property @property
def is_named_volume(self): def is_named_volume(self):
res = self.external and not self.external.startswith(('.', '/', '~')) return self.external and not self.external.startswith(('.', '/', '~'))
if not IS_WINDOWS_PLATFORM:
return res
return (
res and not self.external.startswith('\\') and
not win32_root_path_pattern.match(self.external)
)
class ServiceLink(namedtuple('_ServiceLink', 'target alias')): class ServiceLink(namedtuple('_ServiceLink', 'target alias')):
@ -234,27 +187,3 @@ 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

@ -14,7 +14,6 @@ from jsonschema import FormatChecker
from jsonschema import RefResolver from jsonschema import RefResolver
from jsonschema import ValidationError from jsonschema import ValidationError
from ..const import COMPOSEFILE_V1 as V1
from .errors import ConfigurationError from .errors import ConfigurationError
from .errors import VERSION_EXPLANATION from .errors import VERSION_EXPLANATION
from .sort_services import get_service_name_from_network_mode from .sort_services import get_service_name_from_network_mode
@ -63,6 +62,23 @@ def format_expose(instance):
return True return True
@FormatChecker.cls_checks(format="bool-value-in-mapping")
def format_boolean_in_environment(instance):
"""Check if there is a boolean in the mapping sections and display a warning.
Always return True here so the validation won't raise an error.
"""
if isinstance(instance, bool):
log.warn(
"There is a boolean value in the 'environment', 'labels', or "
"'extra_hosts' field of a service.\n"
"These sections only support string values.\n"
"Please add quotes to any boolean values to make them strings "
"(eg, 'True', 'false', 'yes', 'N', 'on', 'Off').\n"
"This warning will become an error in a future release. \r\n"
)
return True
def match_named_volumes(service_dict, project_volumes): def match_named_volumes(service_dict, project_volumes):
service_volumes = service_dict.get('volumes', []) service_volumes = service_dict.get('volumes', [])
for volume_spec in service_volumes: for volume_spec in service_volumes:
@ -171,22 +187,12 @@ def validate_network_mode(service_config, service_names):
"is undefined.".format(s=service_config, dep=dependency)) "is undefined.".format(s=service_config, dep=dependency))
def validate_links(service_config, service_names):
for link in service_config.config.get('links', []):
if link.split(':')[0] not in service_names:
raise ConfigurationError(
"Service '{s.name}' has a link to service '{link}' which is "
"undefined.".format(s=service_config, link=link))
def validate_depends_on(service_config, service_names): def validate_depends_on(service_config, service_names):
deps = service_config.config.get('depends_on', {}) for dependency in service_config.config.get('depends_on', []):
for dependency in deps.keys():
if dependency not in service_names: if dependency not in service_names:
raise ConfigurationError( raise ConfigurationError(
"Service '{s.name}' depends on service '{dep}' which is " "Service '{s.name}' depends on service '{dep}' which is "
"undefined.".format(s=service_config, dep=dependency) "undefined.".format(s=service_config, dep=dependency))
)
def get_unsupported_config_msg(path, error_key): def get_unsupported_config_msg(path, error_key):
@ -203,7 +209,7 @@ def anglicize_json_type(json_type):
def is_service_dict_schema(schema_id): def is_service_dict_schema(schema_id):
return schema_id in ('config_schema_v1.json', '#/properties/services') return schema_id == 'fields_schema_v1.json' or schema_id == '#/properties/services'
def handle_error_for_schema_with_id(error, path): def handle_error_for_schema_with_id(error, path):
@ -215,16 +221,45 @@ def handle_error_for_schema_with_id(error, path):
list(error.instance)[0], list(error.instance)[0],
VALID_NAME_CHARS) VALID_NAME_CHARS)
if schema_id == '#/definitions/constraints':
# Build context could in 'build' or 'build.context' and dockerfile could be
# in 'dockerfile' or 'build.dockerfile'
context = False
dockerfile = 'dockerfile' in error.instance
if 'build' in error.instance:
if isinstance(error.instance['build'], six.string_types):
context = True
else:
context = 'context' in error.instance['build']
dockerfile = dockerfile or 'dockerfile' in error.instance['build']
# TODO: only applies to v1
if 'image' in error.instance and context:
return (
"{} has both an image and build path specified. "
"A service can either be built to image or use an existing "
"image, not both.".format(path_string(path)))
if 'image' not in error.instance and not context:
return (
"{} has neither an image nor a build path specified. "
"At least one must be provided.".format(path_string(path)))
# TODO: only applies to v1
if 'image' in error.instance and dockerfile:
return (
"{} has both an image and alternate Dockerfile. "
"A service can either be built to image or use an existing "
"image, not both.".format(path_string(path)))
if error.validator == 'additionalProperties': if error.validator == 'additionalProperties':
if schema_id == '#/definitions/service': if schema_id == '#/definitions/service':
invalid_config_key = parse_key_from_error_msg(error) invalid_config_key = parse_key_from_error_msg(error)
return get_unsupported_config_msg(path, invalid_config_key) return get_unsupported_config_msg(path, invalid_config_key)
if not error.path: if not error.path:
return '{}\n\n{}'.format(error.message, VERSION_EXPLANATION) return '{}\n{}'.format(error.message, VERSION_EXPLANATION)
def handle_generic_error(error, path): def handle_generic_service_error(error, path):
msg_format = None msg_format = None
error_msg = error.message error_msg = error.message
@ -330,94 +365,75 @@ def _parse_oneof_validator(error):
return (None, "contains an invalid type, it should be {}".format(valid_types)) return (None, "contains an invalid type, it should be {}".format(valid_types))
def process_service_constraint_errors(error, service_name, version): def process_errors(errors, path_prefix=None):
if version == V1: """jsonschema gives us an error tree full of information to explain what has
if 'image' in error.instance and 'build' in error.instance:
return (
"Service {} has both an image and build path specified. "
"A service can either be built to image or use an existing "
"image, not both.".format(service_name))
if 'image' in error.instance and 'dockerfile' in error.instance:
return (
"Service {} has both an image and alternate Dockerfile. "
"A service can either be built to image or use an existing "
"image, not both.".format(service_name))
if 'image' not in error.instance and 'build' not in error.instance:
return (
"Service {} has neither an image nor a build context specified. "
"At least one must be provided.".format(service_name))
def process_config_schema_errors(error):
path = list(error.path)
if 'id' in error.schema:
error_msg = handle_error_for_schema_with_id(error, path)
if error_msg:
return error_msg
return handle_generic_error(error, path)
def validate_against_config_schema(config_file):
schema = load_jsonschema(config_file.version)
format_checker = FormatChecker(["ports", "expose"])
validator = Draft4Validator(
schema,
resolver=RefResolver(get_resolver_path(), schema),
format_checker=format_checker)
handle_errors(
validator.iter_errors(config_file.config),
process_config_schema_errors,
config_file.filename)
def validate_service_constraints(config, service_name, version):
def handler(errors):
return process_service_constraint_errors(errors, service_name, version)
schema = load_jsonschema(version)
validator = Draft4Validator(schema['definitions']['constraints']['service'])
handle_errors(validator.iter_errors(config), handler, None)
def get_schema_path():
return os.path.dirname(os.path.abspath(__file__))
def load_jsonschema(version):
filename = os.path.join(
get_schema_path(),
"config_schema_v{0}.json".format(version))
with open(filename, "r") as fh:
return json.load(fh)
def get_resolver_path():
schema_path = get_schema_path()
if sys.platform == "win32":
scheme = "///"
# TODO: why is this necessary?
schema_path = schema_path.replace('\\', '/')
else:
scheme = "//"
return "file:{}{}/".format(scheme, schema_path)
def handle_errors(errors, format_error_func, filename):
"""jsonschema returns an error tree full of information to explain what has
gone wrong. Process each error and pull out relevant information and re-write gone wrong. Process each error and pull out relevant information and re-write
helpful error messages that are relevant. helpful error messages that are relevant.
""" """
errors = list(sorted(errors, key=str)) path_prefix = path_prefix or []
def format_error_message(error):
path = path_prefix + list(error.path)
if 'id' in error.schema:
error_msg = handle_error_for_schema_with_id(error, path)
if error_msg:
return error_msg
return handle_generic_service_error(error, path)
return '\n'.join(format_error_message(error) for error in errors)
def validate_against_fields_schema(config_file):
schema_filename = "fields_schema_v{0}.json".format(config_file.version)
_validate_against_schema(
config_file.config,
schema_filename,
format_checker=["ports", "expose", "bool-value-in-mapping"],
filename=config_file.filename)
def validate_against_service_schema(config, service_name, version):
_validate_against_schema(
config,
"service_schema_v{0}.json".format(version),
format_checker=["ports"],
path_prefix=[service_name])
def _validate_against_schema(
config,
schema_filename,
format_checker=(),
path_prefix=None,
filename=None):
config_source_dir = os.path.dirname(os.path.abspath(__file__))
if sys.platform == "win32":
file_pre_fix = "///"
config_source_dir = config_source_dir.replace('\\', '/')
else:
file_pre_fix = "//"
resolver_full_path = "file:{}{}/".format(file_pre_fix, config_source_dir)
schema_file = os.path.join(config_source_dir, schema_filename)
with open(schema_file, "r") as schema_fh:
schema = json.load(schema_fh)
resolver = RefResolver(resolver_full_path, schema)
validation_output = Draft4Validator(
schema,
resolver=resolver,
format_checker=FormatChecker(format_checker))
errors = [error for error in sorted(validation_output.iter_errors(config), key=str)]
if not errors: if not errors:
return return
error_msg = '\n'.join(format_error_func(error) for error in errors) error_msg = process_errors(errors, path_prefix=path_prefix)
raise ConfigurationError( file_msg = " in file '{}'".format(filename) if filename else ''
"The Compose file{file_msg} is invalid because:\n{error_msg}".format( raise ConfigurationError("Validation failed{}, reason(s):\n{}".format(
file_msg=" '{}'".format(filename) if filename else "", file_msg,
error_msg=error_msg)) error_msg))

View file

@ -1,41 +1,29 @@
from __future__ import absolute_import from __future__ import absolute_import
from __future__ import unicode_literals from __future__ import unicode_literals
import os
import sys import sys
DEFAULT_TIMEOUT = 10 DEFAULT_TIMEOUT = 10
HTTP_TIMEOUT = 60 HTTP_TIMEOUT = int(os.environ.get('COMPOSE_HTTP_TIMEOUT', os.environ.get('DOCKER_CLIENT_TIMEOUT', 60)))
IMAGE_EVENTS = ['delete', 'import', 'load', 'pull', 'push', 'save', 'tag', 'untag'] IMAGE_EVENTS = ['delete', 'import', 'pull', 'push', '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'
LABEL_PROJECT = 'com.docker.compose.project' LABEL_PROJECT = 'com.docker.compose.project'
LABEL_SERVICE = 'com.docker.compose.service' LABEL_SERVICE = 'com.docker.compose.service'
LABEL_NETWORK = 'com.docker.compose.network'
LABEL_VERSION = 'com.docker.compose.version' LABEL_VERSION = 'com.docker.compose.version'
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_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_V3_0: '1.25',
COMPOSEFILE_V3_1: '1.25',
} }
API_VERSION_TO_ENGINE_VERSION = { API_VERSION_TO_ENGINE_VERSION = {
API_VERSIONS[COMPOSEFILE_V1]: '1.9.0', API_VERSIONS[COMPOSEFILE_V1]: '1.9.0',
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_V3_0]: '1.13.0',
API_VERSIONS[COMPOSEFILE_V3_1]: '1.13.0',
} }

View file

@ -39,7 +39,7 @@ class Container(object):
@classmethod @classmethod
def from_id(cls, client, id): def from_id(cls, client, id):
return cls(client, client.inspect_container(id), has_been_inspected=True) return cls(client, client.inspect_container(id))
@classmethod @classmethod
def create(cls, client, **options): def create(cls, client, **options):
@ -163,7 +163,7 @@ class Container(object):
@property @property
def has_api_logs(self): def has_api_logs(self):
log_type = self.log_driver log_type = self.log_driver
return not log_type or log_type in ('json-file', 'journald') return not log_type or log_type != 'none'
def attach_log_stream(self): def attach_log_stream(self):
"""A log stream can only be attached if the container uses a json-file """A log stream can only be attached if the container uses a json-file
@ -216,12 +216,6 @@ class Container(object):
def remove(self, **options): def remove(self, **options):
return self.client.remove_container(self.id, **options) return self.client.remove_container(self.id, **options)
def create_exec(self, command, **options):
return self.client.exec_create(self.id, command, **options)
def start_exec(self, exec_id, **options):
return self.client.exec_start(exec_id, **options)
def rename_to_tmp_name(self): def rename_to_tmp_name(self):
"""Rename the container to a hopefully unique temporary container name """Rename the container to a hopefully unique temporary container name
by prepending the short id. by prepending the short id.

View file

@ -1,33 +0,0 @@
from __future__ import absolute_import
from __future__ import unicode_literals
class OperationFailedError(Exception):
def __init__(self, reason):
self.msg = reason
class StreamParseError(RuntimeError):
def __init__(self, reason):
self.msg = reason
class HealthCheckException(Exception):
def __init__(self, reason):
self.msg = reason
class HealthCheckFailed(HealthCheckException):
def __init__(self, container_id):
super(HealthCheckFailed, self).__init__(
'Container "{}" is unhealthy.'.format(container_id)
)
class NoHealthCheckConfigured(HealthCheckException):
def __init__(self, service_name):
super(NoHealthCheckConfigured, self).__init__(
'Service "{}" is missing a healthcheck configuration'.format(
service_name
)
)

View file

@ -4,27 +4,18 @@ from __future__ import unicode_literals
import logging import logging
from docker.errors import NotFound from docker.errors import NotFound
from docker.types import IPAMConfig from docker.utils import create_ipam_config
from docker.types import IPAMPool from docker.utils import create_ipam_pool
from docker.utils import version_gte
from docker.utils import version_lt
from .config import ConfigurationError from .config import ConfigurationError
from .const import LABEL_NETWORK
from .const import LABEL_PROJECT
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
OPTS_EXCEPTIONS = [
'com.docker.network.driver.overlay.vxlanid_list',
]
class Network(object): class Network(object):
def __init__(self, client, project, name, driver=None, driver_opts=None, def __init__(self, client, project, name, driver=None, driver_opts=None,
ipam=None, external_name=None, internal=False, enable_ipv6=False, ipam=None, external_name=None):
labels=None):
self.client = client self.client = client
self.project = project self.project = project
self.name = name self.name = name
@ -32,9 +23,6 @@ class Network(object):
self.driver_opts = driver_opts self.driver_opts = driver_opts
self.ipam = create_ipam_config_from_dict(ipam) self.ipam = create_ipam_config_from_dict(ipam)
self.external_name = external_name self.external_name = external_name
self.internal = internal
self.enable_ipv6 = enable_ipv6
self.labels = labels
def ensure(self): def ensure(self):
if self.external_name: if self.external_name:
@ -57,7 +45,14 @@ class Network(object):
try: try:
data = self.inspect() data = self.inspect()
check_remote_network_config(data, self) if self.driver and data['Driver'] != self.driver:
raise ConfigurationError(
'Network "{}" needs to be recreated - driver has changed'
.format(self.full_name))
if data['Options'] != (self.driver_opts or {}):
raise ConfigurationError(
'Network "{}" needs to be recreated - options have changed'
.format(self.full_name))
except NotFound: except NotFound:
driver_name = 'the default driver' driver_name = 'the default driver'
if self.driver: if self.driver:
@ -73,10 +68,6 @@ class Network(object):
driver=self.driver, driver=self.driver,
options=self.driver_opts, options=self.driver_opts,
ipam=self.ipam, ipam=self.ipam,
internal=self.internal,
enable_ipv6=self.enable_ipv6,
labels=self._labels,
attachable=version_gte(self.client._version, '1.24') or None,
) )
def remove(self): def remove(self):
@ -96,26 +87,15 @@ class Network(object):
return self.external_name return self.external_name
return '{0}_{1}'.format(self.project, self.name) return '{0}_{1}'.format(self.project, self.name)
@property
def _labels(self):
if version_lt(self.client._version, '1.23'):
return None
labels = self.labels.copy() if self.labels else {}
labels.update({
LABEL_PROJECT: self.project,
LABEL_NETWORK: self.name,
})
return labels
def create_ipam_config_from_dict(ipam_dict): def create_ipam_config_from_dict(ipam_dict):
if not ipam_dict: if not ipam_dict:
return None return None
return IPAMConfig( return create_ipam_config(
driver=ipam_dict.get('driver'), driver=ipam_dict.get('driver'),
pool_configs=[ pool_configs=[
IPAMPool( create_ipam_pool(
subnet=config.get('subnet'), subnet=config.get('subnet'),
iprange=config.get('ip_range'), iprange=config.get('ip_range'),
gateway=config.get('gateway'), gateway=config.get('gateway'),
@ -126,24 +106,6 @@ def create_ipam_config_from_dict(ipam_dict):
) )
def check_remote_network_config(remote, local):
if local.driver and remote.get('Driver') != local.driver:
raise ConfigurationError(
'Network "{}" needs to be recreated - driver has changed'
.format(local.full_name)
)
local_opts = local.driver_opts or {}
remote_opts = remote.get('Options') or {}
for k in set.union(set(remote_opts.keys()), set(local_opts.keys())):
if k in OPTS_EXCEPTIONS:
continue
if remote_opts.get(k) != local_opts.get(k):
raise ConfigurationError(
'Network "{}" needs to be recreated - options have changed'
.format(local.full_name)
)
def build_networks(name, config_data, client): def build_networks(name, config_data, client):
network_config = config_data.networks or {} network_config = config_data.networks or {}
networks = { networks = {
@ -153,9 +115,6 @@ def build_networks(name, config_data, client):
driver_opts=data.get('driver_opts'), driver_opts=data.get('driver_opts'),
ipam=data.get('ipam'), ipam=data.get('ipam'),
external_name=data.get('external_name'), external_name=data.get('external_name'),
internal=data.get('internal'),
enable_ipv6=data.get('enable_ipv6'),
labels=data.get('labels'),
) )
for network_name, data in network_config.items() for network_name, data in network_config.items()
} }
@ -190,10 +149,7 @@ class ProjectNetworks(object):
if not self.use_networking: if not self.use_networking:
return return
for network in self.networks.values(): for network in self.networks.values():
try: network.remove()
network.remove()
except NotFound:
log.warn("Network %s not found.", network.full_name)
def initialize(self): def initialize(self):
if not self.use_networking: if not self.use_networking:
@ -203,26 +159,26 @@ class ProjectNetworks(object):
network.ensure() network.ensure()
def get_network_defs_for_service(service_dict): def get_network_aliases_for_service(service_dict):
if 'network_mode' in service_dict: if 'network_mode' in service_dict:
return {} return {}
networks = service_dict.get('networks', {'default': None}) networks = service_dict.get('networks', {'default': None})
return dict( return dict(
(net, (config or {})) (net, (config or {}).get('aliases', []))
for net, config in networks.items() for net, config in networks.items()
) )
def get_network_names_for_service(service_dict): def get_network_names_for_service(service_dict):
return get_network_defs_for_service(service_dict).keys() return get_network_aliases_for_service(service_dict).keys()
def get_networks(service_dict, network_definitions): def get_networks(service_dict, network_definitions):
networks = {} networks = {}
for name, netdef in get_network_defs_for_service(service_dict).items(): for name, aliases in get_network_aliases_for_service(service_dict).items():
network = network_definitions.get(name) network = network_definitions.get(name)
if network: if network:
networks[network.full_name] = netdef networks[network.full_name] = aliases
else: else:
raise ConfigurationError( raise ConfigurationError(
'Service "{}" uses an undefined network "{}"' 'Service "{}" uses an undefined network "{}"'

View file

@ -1,199 +1,71 @@
from __future__ import absolute_import from __future__ import absolute_import
from __future__ import unicode_literals from __future__ import unicode_literals
import logging
import operator import operator
import sys import sys
from threading import Thread from threading import Thread
from docker.errors import APIError from docker.errors import APIError
from six.moves import _thread as thread
from six.moves.queue import Empty 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.errors import HealthCheckFailed
from compose.errors import NoHealthCheckConfigured
from compose.errors import OperationFailedError
from compose.utils import get_output_stream from compose.utils import get_output_stream
log = logging.getLogger(__name__) def perform_operation(func, arg, callback, index):
try:
STOP = object() callback((index, func(arg)))
except Exception as e:
callback((index, e))
def parallel_execute(objects, func, get_name, msg, get_deps=None): def parallel_execute(objects, func, index_func, msg):
"""Runs func on objects in parallel while ensuring that func is """For a given list of objects, call the callable passing in the first
ran on object only after it is ran on all its dependencies. object we give it.
get_deps called on object must return a collection with its dependencies.
get_name called on object must return its name.
""" """
objects = list(objects) objects = list(objects)
stream = get_output_stream(sys.stderr) stream = get_output_stream(sys.stderr)
writer = ParallelStreamWriter(stream, msg) writer = ParallelStreamWriter(stream, msg)
for obj in objects: for obj in objects:
writer.initialize(get_name(obj)) writer.initialize(index_func(obj))
events = parallel_execute_iter(objects, func, get_deps) q = Queue()
# TODO: limit the number of threads #1828
for obj in objects:
t = Thread(
target=perform_operation,
args=(func, obj, q.put, index_func(obj)))
t.daemon = True
t.start()
done = 0
errors = {} errors = {}
results = []
error_to_reraise = None
for obj, result, exception in events:
if exception is None:
writer.write(get_name(obj), 'done')
results.append(result)
elif isinstance(exception, APIError):
errors[get_name(obj)] = exception.explanation
writer.write(get_name(obj), 'error')
elif isinstance(exception, (OperationFailedError, HealthCheckFailed, NoHealthCheckConfigured)):
errors[get_name(obj)] = exception.msg
writer.write(get_name(obj), 'error')
elif isinstance(exception, UpstreamError):
writer.write(get_name(obj), 'error')
else:
errors[get_name(obj)] = exception
error_to_reraise = exception
for obj_name, error in errors.items():
stream.write("\nERROR: for {} {}\n".format(obj_name, error))
if error_to_reraise:
raise error_to_reraise
return results, errors
def _no_deps(x):
return []
class State(object):
"""
Holds the state of a partially-complete parallel operation.
state.started: objects being processed
state.finished: objects which have been processed
state.failed: objects which either failed or whose dependencies failed
"""
def __init__(self, objects):
self.objects = objects
self.started = set()
self.finished = set()
self.failed = set()
def is_done(self):
return len(self.finished) + len(self.failed) >= len(self.objects)
def pending(self):
return set(self.objects) - self.started - self.finished - self.failed
def parallel_execute_iter(objects, func, get_deps):
"""
Runs func on objects in parallel while ensuring that func is
ran on object only after it is ran on all its dependencies.
Returns an iterator of tuples which look like:
# if func returned normally when run on object
(object, result, None)
# if func raised an exception when run on object
(object, None, exception)
# if func raised an exception when run on one of object's dependencies
(object, None, UpstreamError())
"""
if get_deps is None:
get_deps = _no_deps
results = Queue()
state = State(objects)
while True:
feed_queue(objects, func, get_deps, results, state)
while done < len(objects):
try: try:
event = results.get(timeout=0.1) msg_index, result = q.get(timeout=1)
except Empty: except Empty:
continue continue
# See https://github.com/docker/compose/issues/189
except thread.error:
raise ShutdownException()
if event is STOP: if isinstance(result, APIError):
break errors[msg_index] = "error", result.explanation
writer.write(msg_index, 'error')
obj, _, exception = event elif isinstance(result, Exception):
if exception is None: errors[msg_index] = "unexpected_exception", result
log.debug('Finished processing: {}'.format(obj))
state.finished.add(obj)
else: else:
log.debug('Failed: {}'.format(obj)) writer.write(msg_index, 'done')
state.failed.add(obj) done += 1
yield event if not errors:
return
stream.write("\n")
def producer(obj, func, results): for msg_index, (result, error) in errors.items():
""" stream.write("ERROR: for {} {} \n".format(msg_index, error))
The entry point for a producer thread which runs func on a single object. if result == 'unexpected_exception':
Places a tuple on the results queue once func has either returned or raised. raise error
"""
try:
result = func(obj)
results.put((obj, result, None))
except Exception as e:
results.put((obj, None, e))
def feed_queue(objects, func, get_deps, results, state):
"""
Starts producer threads for any objects which are ready to be processed
(i.e. they have no dependencies which haven't been successfully processed).
Shortcuts any objects whose dependencies have failed and places an
(object, None, UpstreamError()) tuple on the results queue.
"""
pending = state.pending()
log.debug('Pending: {}'.format(pending))
for obj in pending:
deps = get_deps(obj)
try:
if any(dep[0] in state.failed for dep in deps):
log.debug('{} has upstream errors - not processing'.format(obj))
results.put((obj, None, UpstreamError()))
state.failed.add(obj)
elif all(
dep not in objects or (
dep in state.finished and (not ready_check or ready_check(dep))
) for dep, ready_check in deps
):
log.debug('Starting producer thread for {}'.format(obj))
t = Thread(target=producer, args=(obj, func, results))
t.daemon = True
t.start()
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():
results.put(STOP)
class UpstreamError(Exception):
pass
class ParallelStreamWriter(object): class ParallelStreamWriter(object):
@ -209,15 +81,11 @@ class ParallelStreamWriter(object):
self.lines = [] self.lines = []
def initialize(self, obj_index): def initialize(self, obj_index):
if self.msg is None:
return
self.lines.append(obj_index) self.lines.append(obj_index)
self.stream.write("{} {} ... \r\n".format(self.msg, obj_index)) self.stream.write("{} {} ... \r\n".format(self.msg, obj_index))
self.stream.flush() self.stream.flush()
def write(self, obj_index, status): def write(self, obj_index, status):
if self.msg is None:
return
position = self.lines.index(obj_index) position = self.lines.index(obj_index)
diff = len(self.lines) - position diff = len(self.lines) - position
# move up # move up
@ -243,6 +111,10 @@ def parallel_remove(containers, options):
parallel_operation(stopped_containers, 'remove', options, 'Removing') parallel_operation(stopped_containers, 'remove', options, 'Removing')
def parallel_stop(containers, options):
parallel_operation(containers, 'stop', options, 'Stopping')
def parallel_start(containers, options): def parallel_start(containers, options):
parallel_operation(containers, 'start', options, 'Starting') parallel_operation(containers, 'start', options, 'Starting')
@ -257,3 +129,7 @@ def parallel_unpause(containers, options):
def parallel_kill(containers, options): def parallel_kill(containers, options):
parallel_operation(containers, 'kill', options, 'Killing') parallel_operation(containers, 'kill', options, 'Killing')
def parallel_restart(containers, options):
parallel_operation(containers, 'restart', options, 'Restarting')

View file

@ -32,11 +32,12 @@ def stream_output(output, stream):
if not image_id: if not image_id:
continue continue
if image_id not in lines: if image_id in lines:
diff = len(lines) - lines[image_id]
else:
lines[image_id] = len(lines) lines[image_id] = len(lines)
stream.write("\n") stream.write("\n")
diff = 0
diff = len(lines) - lines[image_id]
# move cursor up `diff` rows # move cursor up `diff` rows
stream.write("%c[%dA" % (27, diff)) stream.write("%c[%dA" % (27, diff))
@ -90,22 +91,3 @@ def print_output_event(event, stream, is_terminal):
stream.write("%s%s" % (event['stream'], terminator)) stream.write("%s%s" % (event['stream'], terminator))
else: else:
stream.write("%s%s\n" % (status, terminator)) stream.write("%s%s\n" % (status, terminator))
def get_digest_from_pull(events):
for event in events:
status = event.get('status')
if not status or 'Digest' not in status:
continue
_, digest = status.split(':', 1)
return digest.strip()
return None
def get_digest_from_push(events):
for event in events:
digest = event.get('aux', {}).get('Digest')
if digest:
return digest
return None

View file

@ -3,10 +3,8 @@ from __future__ import unicode_literals
import datetime import datetime
import logging import logging
import operator
from functools import reduce from functools import reduce
import enum
from docker.errors import APIError from docker.errors import APIError
from . import parallel from . import parallel
@ -14,6 +12,7 @@ from .config import ConfigurationError
from .config.config import V1 from .config.config import V1
from .config.sort_services import get_container_name_from_network_mode from .config.sort_services import get_container_name_from_network_mode
from .config.sort_services import get_service_name_from_network_mode from .config.sort_services import get_service_name_from_network_mode
from .const import DEFAULT_TIMEOUT
from .const import IMAGE_EVENTS from .const import IMAGE_EVENTS
from .const import LABEL_ONE_OFF from .const import LABEL_ONE_OFF
from .const import LABEL_PROJECT from .const import LABEL_PROJECT
@ -22,7 +21,6 @@ from .container import Container
from .network import build_networks from .network import build_networks
from .network import get_networks from .network import get_networks
from .network import ProjectNetworks from .network import ProjectNetworks
from .service import BuildAction
from .service import ContainerNetworkMode from .service import ContainerNetworkMode
from .service import ConvergenceStrategy from .service import ConvergenceStrategy
from .service import NetworkMode from .service import NetworkMode
@ -35,24 +33,6 @@ from .volume import ProjectVolumes
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@enum.unique
class OneOffFilter(enum.Enum):
include = 0
exclude = 1
only = 2
@classmethod
def update_labels(cls, value, labels):
if value == cls.only:
labels.append('{0}={1}'.format(LABEL_ONE_OFF, "True"))
elif value == cls.exclude:
labels.append('{0}={1}'.format(LABEL_ONE_OFF, "False"))
elif value == cls.include:
pass
else:
raise ValueError("Invalid value for one_off: {}".format(repr(value)))
class Project(object): class Project(object):
""" """
A collection of services. A collection of services.
@ -64,11 +44,11 @@ class Project(object):
self.volumes = volumes or ProjectVolumes({}) self.volumes = volumes or ProjectVolumes({})
self.networks = networks or ProjectNetworks({}, False) self.networks = networks or ProjectNetworks({}, False)
def labels(self, one_off=OneOffFilter.exclude): def labels(self, one_off=False):
labels = ['{0}={1}'.format(LABEL_PROJECT, self.name)] return [
'{0}={1}'.format(LABEL_PROJECT, self.name),
OneOffFilter.update_labels(one_off, labels) '{0}={1}'.format(LABEL_ONE_OFF, "True" if one_off else "False"),
return labels ]
@classmethod @classmethod
def from_config(cls, name, config_data, client): def from_config(cls, name, config_data, client):
@ -104,11 +84,6 @@ 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'),
@ -119,7 +94,6 @@ 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)
) )
@ -225,43 +199,13 @@ class Project(object):
def start(self, service_names=None, **options): def start(self, service_names=None, **options):
containers = [] containers = []
for service in self.get_services(service_names):
def start_service(service): service_containers = service.start(**options)
service_containers = service.start(quiet=True, **options)
containers.extend(service_containers) containers.extend(service_containers)
services = self.get_services(service_names)
def get_deps(service):
return {
(self.get_service(dep), config)
for dep, config in service.get_dependency_configs().items()
}
parallel.parallel_execute(
services,
start_service,
operator.attrgetter('name'),
'Starting',
get_deps)
return containers return containers
def stop(self, service_names=None, one_off=OneOffFilter.exclude, **options): def stop(self, service_names=None, **options):
containers = self.containers(service_names, one_off=one_off) parallel.parallel_stop(self.containers(service_names), options)
def get_deps(container):
# actually returning inversed dependencies
return {(other, None) for other in containers
if container.service in
self.get_service(other.service).get_dependency_names()}
parallel.parallel_execute(
containers,
self.build_container_operation_with_timeout_func('stop', options),
operator.attrgetter('name'),
'Stopping',
get_deps)
def pause(self, service_names=None, **options): def pause(self, service_names=None, **options):
containers = self.containers(service_names) containers = self.containers(service_names)
@ -276,16 +220,12 @@ class Project(object):
def kill(self, service_names=None, **options): def kill(self, service_names=None, **options):
parallel.parallel_kill(self.containers(service_names), options) parallel.parallel_kill(self.containers(service_names), options)
def remove_stopped(self, service_names=None, one_off=OneOffFilter.exclude, **options): def remove_stopped(self, service_names=None, **options):
parallel.parallel_remove(self.containers( parallel.parallel_remove(self.containers(service_names, stopped=True), options)
service_names, stopped=True, one_off=one_off
), options)
def down(self, remove_image_type, include_volumes, remove_orphans=False):
self.stop(one_off=OneOffFilter.include)
self.find_orphan_containers(remove_orphans)
self.remove_stopped(v=include_volumes, one_off=OneOffFilter.include)
def down(self, remove_image_type, include_volumes):
self.stop()
self.remove_stopped(v=include_volumes)
self.networks.remove() self.networks.remove()
if include_volumes: if include_volumes:
@ -299,12 +239,7 @@ class Project(object):
def restart(self, service_names=None, **options): def restart(self, service_names=None, **options):
containers = self.containers(service_names, stopped=True) containers = self.containers(service_names, stopped=True)
parallel.parallel_restart(containers, options)
parallel.parallel_execute(
containers,
self.build_container_operation_with_timeout_func('restart', options),
operator.attrgetter('name'),
'Restarting')
return containers return containers
def build(self, service_names=None, no_cache=False, pull=False, force_rm=False): def build(self, service_names=None, no_cache=False, pull=False, force_rm=False):
@ -314,25 +249,19 @@ class Project(object):
else: else:
log.info('%s uses an image, skipping' % service.name) log.info('%s uses an image, skipping' % service.name)
def create( def create(self, service_names=None, strategy=ConvergenceStrategy.changed, do_build=True):
self,
service_names=None,
strategy=ConvergenceStrategy.changed,
do_build=BuildAction.none,
):
services = self.get_services_without_duplicate(service_names, include_deps=True) services = self.get_services_without_duplicate(service_names, include_deps=True)
for svc in services:
svc.ensure_image_exists(do_build=do_build)
plans = self._get_convergence_plans(services, strategy) plans = self._get_convergence_plans(services, strategy)
for service in services: for service in services:
service.execute_convergence_plan( service.execute_convergence_plan(
plans[service.name], plans[service.name],
do_build,
detached=True, detached=True,
start=False) start=False)
def events(self, service_names=None): def events(self):
def build_container_event(event, container): def build_container_event(event, container):
time = datetime.datetime.fromtimestamp(event['time']) time = datetime.datetime.fromtimestamp(event['time'])
time = time.replace( time = time.replace(
@ -346,29 +275,21 @@ class Project(object):
'attributes': { 'attributes': {
'name': container.name, 'name': container.name,
'image': event['from'], 'image': event['from'],
}, }
'container': container,
} }
service_names = set(service_names or self.service_names) service_names = set(self.service_names)
for event in self.client.events( for event in self.client.events(
filters={'label': self.labels()}, filters={'label': self.labels()},
decode=True decode=True
): ):
# The first part of this condition is a guard against some events if event['status'] in IMAGE_EVENTS:
# broadcasted by swarm that don't have a status field.
# See https://github.com/docker/compose/issues/3316
if 'status' not in event or event['status'] in IMAGE_EVENTS:
# We don't receive any image events because labels aren't applied # We don't receive any image events because labels aren't applied
# to images # to images
continue continue
# 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: container = Container.from_id(self.client, event['id'])
# this can fail if the container has been removed
container = Container.from_id(self.client, event['id'])
except APIError:
continue
if container.service not in service_names: if container.service not in service_names:
continue continue
yield build_container_event(event, container) yield build_container_event(event, container)
@ -377,54 +298,25 @@ class Project(object):
service_names=None, service_names=None,
start_deps=True, start_deps=True,
strategy=ConvergenceStrategy.changed, strategy=ConvergenceStrategy.changed,
do_build=BuildAction.none, do_build=True,
timeout=None, timeout=DEFAULT_TIMEOUT,
detached=False, detached=False):
remove_orphans=False):
warn_for_swarm_mode(self.client)
self.initialize() self.initialize()
self.find_orphan_containers(remove_orphans)
services = self.get_services_without_duplicate( services = self.get_services_without_duplicate(
service_names, service_names,
include_deps=start_deps) include_deps=start_deps)
for svc in services:
svc.ensure_image_exists(do_build=do_build)
plans = self._get_convergence_plans(services, strategy) plans = self._get_convergence_plans(services, strategy)
return [
def do(service): container
return service.execute_convergence_plan( for service in services
for container in service.execute_convergence_plan(
plans[service.name], plans[service.name],
do_build=do_build,
timeout=timeout, timeout=timeout,
detached=detached detached=detached
) )
def get_deps(service):
return {
(self.get_service(dep), config)
for dep, config in service.get_dependency_configs().items()
}
results, errors = parallel.parallel_execute(
services,
do,
operator.attrgetter('name'),
None,
get_deps
)
if errors:
raise ProjectError(
'Encountered errors while bringing up the project.'
)
return [
container
for svc_containers in results
if svc_containers is not None
for container in svc_containers
] ]
def initialize(self): def initialize(self):
@ -458,56 +350,23 @@ class Project(object):
for service in self.get_services(service_names, include_deps=False): for service in self.get_services(service_names, include_deps=False):
service.pull(ignore_pull_failures) service.pull(ignore_pull_failures)
def push(self, service_names=None, ignore_push_failures=False): def containers(self, service_names=None, stopped=False, one_off=False):
for service in self.get_services(service_names, include_deps=False):
service.push(ignore_push_failures)
def _labeled_containers(self, stopped=False, one_off=OneOffFilter.exclude):
return list(filter(None, [
Container.from_ps(self.client, container)
for container in self.client.containers(
all=stopped,
filters={'label': self.labels(one_off=one_off)})])
)
def containers(self, service_names=None, stopped=False, one_off=OneOffFilter.exclude):
if service_names: if service_names:
self.validate_service_names(service_names) self.validate_service_names(service_names)
else: else:
service_names = self.service_names service_names = self.service_names
containers = self._labeled_containers(stopped, one_off) containers = list(filter(None, [
Container.from_ps(self.client, container)
for container in self.client.containers(
all=stopped,
filters={'label': self.labels(one_off=one_off)})]))
def matches_service_names(container): def matches_service_names(container):
return container.labels.get(LABEL_SERVICE) in service_names return container.labels.get(LABEL_SERVICE) in service_names
return [c for c in containers if matches_service_names(c)] return [c for c in containers if matches_service_names(c)]
def find_orphan_containers(self, remove_orphans):
def _find():
containers = self._labeled_containers()
for ctnr in containers:
service_name = ctnr.labels.get(LABEL_SERVICE)
if service_name not in self.service_names:
yield ctnr
orphans = list(_find())
if not orphans:
return
if remove_orphans:
for ctnr in orphans:
log.info('Removing orphan container "{0}"'.format(ctnr.name))
ctnr.kill()
ctnr.remove(force=True)
else:
log.warning(
'Found orphan containers ({0}) for this project. If '
'you removed or renamed this service in your compose '
'file, you can run this command with the '
'--remove-orphans flag to clean it up.'.format(
', '.join(["{}".format(ctnr.name) for ctnr in orphans])
)
)
def _inject_deps(self, acc, service): def _inject_deps(self, acc, service):
dep_names = service.get_dependency_names() dep_names = service.get_dependency_names()
@ -522,14 +381,6 @@ class Project(object):
dep_services.append(service) dep_services.append(service)
return acc + dep_services return acc + dep_services
def build_container_operation_with_timeout_func(self, operation, options):
def container_operation_with_timeout(container):
if options.get('timeout') is None:
service = self.get_service(container.service)
options['timeout'] = service.stop_timeout(None)
return getattr(container, operation)(**options)
return container_operation_with_timeout
def get_volumes_from(project, service_dict): def get_volumes_from(project, service_dict):
volumes_from = service_dict.pop('volumes_from', None) volumes_from = service_dict.pop('volumes_from', None)
@ -559,49 +410,6 @@ 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):
info = client.info()
if info.get('Swarm', {}).get('LocalNodeState') == 'active':
if info.get('ServerVersion', '').startswith('ucp'):
# UCP does multi-node scheduling with traditional Compose files.
return
log.warn(
"The Docker Engine you're using is running in swarm mode.\n\n"
"Compose does not use swarm mode to deploy services to multiple nodes in a swarm. "
"All containers will be scheduled on the current node.\n\n"
"To deploy your application across the swarm, "
"use `docker stack deploy`.\n"
)
class NoSuchService(Exception): class NoSuchService(Exception):
def __init__(self, name): def __init__(self, name):
self.name = name self.name = name
@ -609,8 +417,3 @@ class NoSuchService(Exception):
def __str__(self): def __str__(self):
return self.msg return self.msg
class ProjectError(Exception):
def __init__(self, msg):
self.msg = msg

View file

@ -10,20 +10,15 @@ 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.utils import LogConfig
from docker.errors import NotFound
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 .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
@ -31,15 +26,11 @@ from .const import LABEL_PROJECT
from .const import LABEL_SERVICE from .const import LABEL_SERVICE
from .const import LABEL_VERSION from .const import LABEL_VERSION
from .container import Container from .container import Container
from .errors import HealthCheckFailed
from .errors import NoHealthCheckConfigured
from .errors import OperationFailedError
from .parallel import parallel_execute from .parallel import parallel_execute
from .parallel import parallel_start from .parallel import parallel_start
from .progress_stream import stream_output from .progress_stream import stream_output
from .progress_stream import StreamOutputError from .progress_stream import StreamOutputError
from .utils import json_hash from .utils import json_hash
from .utils import parse_seconds_float
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -49,34 +40,25 @@ DOCKER_START_KEYS = [
'cap_add', 'cap_add',
'cap_drop', 'cap_drop',
'cgroup_parent', 'cgroup_parent',
'cpu_quota',
'devices', 'devices',
'dns', 'dns',
'dns_search', 'dns_search',
'env_file', 'env_file',
'extra_hosts', 'extra_hosts',
'group_add',
'ipc', 'ipc',
'read_only', 'read_only',
'log_driver', 'log_driver',
'log_opt', 'log_opt',
'mem_limit', 'mem_limit',
'memswap_limit', 'memswap_limit',
'oom_score_adj',
'mem_swappiness',
'pid', 'pid',
'privileged', 'privileged',
'restart', 'restart',
'security_opt',
'shm_size',
'sysctls',
'userns_mode',
'volumes_from', 'volumes_from',
'security_opt',
'cpu_quota',
] ]
CONDITION_STARTED = 'service_started'
CONDITION_HEALTHY = 'service_healthy'
class BuildError(Exception): class BuildError(Exception):
def __init__(self, service, reason): def __init__(self, service, reason):
@ -121,14 +103,6 @@ class ImageType(enum.Enum):
all = 2 all = 2
@enum.unique
class BuildAction(enum.Enum):
"""Enumeration for the possible build actions."""
none = 0
force = 1
skip = 2
class Service(object): class Service(object):
def __init__( def __init__(
self, self,
@ -140,7 +114,6 @@ 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
@ -151,12 +124,8 @@ 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):
return '<Service: {}>'.format(self.name)
def containers(self, stopped=False, one_off=False, filters={}): def containers(self, stopped=False, one_off=False, filters={}):
filters.update({'label': self.labels(one_off=one_off)}) filters.update({'label': self.labels(one_off=one_off)})
@ -182,7 +151,7 @@ class Service(object):
self.start_container_if_stopped(c, **options) self.start_container_if_stopped(c, **options)
return containers return containers
def scale(self, desired_num, timeout=None): def scale(self, desired_num, timeout=DEFAULT_TIMEOUT):
""" """
Adjusts the number of containers to the specified number and ensures Adjusts the number of containers to the specified number and ensures
they are running. they are running.
@ -192,13 +161,13 @@ class Service(object):
- starts containers until there are at least `desired_num` running - starts containers until there are at least `desired_num` running
- removes all stopped containers - removes all stopped containers
""" """
if self.custom_container_name and desired_num > 1: if self.custom_container_name() and desired_num > 1:
log.warn('The "%s" service is using the custom container name "%s". ' log.warn('The "%s" service is using the custom container name "%s". '
'Docker requires each container to have a unique name. ' 'Docker requires each container to have a unique name. '
'Remove the custom name to scale the service.' 'Remove the custom name to scale the service.'
% (self.name, self.custom_container_name)) % (self.name, self.custom_container_name()))
if self.specifies_host_port() and desired_num > 1: if self.specifies_host_port():
log.warn('The "%s" service specifies a port on the host. If multiple containers ' log.warn('The "%s" service specifies a port on the host. If multiple containers '
'for this service are created on a single host, the port will clash.' 'for this service are created on a single host, the port will clash.'
% self.name) % self.name)
@ -209,7 +178,7 @@ class Service(object):
return container return container
def stop_and_remove(container): def stop_and_remove(container):
container.stop(timeout=self.stop_timeout(timeout)) container.stop(timeout=timeout)
container.remove() container.remove()
running_containers = self.containers(stopped=False) running_containers = self.containers(stopped=False)
@ -226,9 +195,7 @@ class Service(object):
if num_running != len(all_containers): if num_running != len(all_containers):
# we have some stopped containers, let's start them up again # we have some stopped containers, let's start them up again
stopped_containers = sorted( stopped_containers = sorted([c for c in all_containers if not c.is_running], key=attrgetter('number'))
(c for c in all_containers if not c.is_running),
key=attrgetter('number'))
num_stopped = len(stopped_containers) num_stopped = len(stopped_containers)
@ -253,7 +220,7 @@ class Service(object):
parallel_execute( parallel_execute(
container_numbers, container_numbers,
lambda n: create_and_start(service=self, number=n), lambda n: create_and_start(service=self, number=n),
lambda n: self.get_container_name(n), lambda n: n,
"Creating and starting" "Creating and starting"
) )
@ -273,6 +240,7 @@ class Service(object):
def create_container(self, def create_container(self,
one_off=False, one_off=False,
do_build=True,
previous_container=None, previous_container=None,
number=None, number=None,
quiet=False, quiet=False,
@ -281,9 +249,7 @@ class Service(object):
Create a container for this service. If the image doesn't exist, attempt to pull Create a container for this service. If the image doesn't exist, attempt to pull
it. it.
""" """
# This is only necessary for `scale` and `volumes_from` self.ensure_image_exists(do_build=do_build)
# auto-creating containers to satisfy the dependency.
self.ensure_image_exists()
container_options = self._get_container_create_options( container_options = self._get_container_create_options(
override_options, override_options,
@ -295,41 +261,31 @@ class Service(object):
if 'name' in container_options and not quiet: if 'name' in container_options and not quiet:
log.info("Creating %s" % container_options['name']) log.info("Creating %s" % container_options['name'])
try: return Container.create(self.client, **container_options)
return Container.create(self.client, **container_options)
except APIError as ex:
raise OperationFailedError("Cannot create container for service %s: %s" %
(self.name, ex.explanation))
def ensure_image_exists(self, do_build=BuildAction.none):
if self.can_be_built() and do_build == BuildAction.force:
self.build()
return
def ensure_image_exists(self, do_build=True):
try: try:
self.image() self.image()
return return
except NoSuchImageError: except NoSuchImageError:
pass pass
if not self.can_be_built(): if self.can_be_built():
if do_build:
self.build()
else:
raise NeedsBuildError(self)
else:
self.pull() self.pull()
return
if do_build == BuildAction.skip:
raise NeedsBuildError(self)
self.build()
log.warn(
"Image for service {} was built because it did not already exist. To "
"rebuild this image you must use `docker-compose build` or "
"`docker-compose up --build`.".format(self.name))
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 ImageNotFound: except APIError as e:
raise NoSuchImageError("Image '{}' not found".format(self.image_name)) 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))
else:
raise
@property @property
def image_name(self): def image_name(self):
@ -384,14 +340,15 @@ class Service(object):
def execute_convergence_plan(self, def execute_convergence_plan(self,
plan, plan,
timeout=None, do_build=True,
timeout=DEFAULT_TIMEOUT,
detached=False, detached=False,
start=True): start=True):
(action, containers) = plan (action, containers) = plan
should_attach_logs = not detached should_attach_logs = not detached
if action == 'create': if action == 'create':
container = self.create_container() container = self.create_container(do_build=do_build)
if should_attach_logs: if should_attach_logs:
container.attach_log_stream() container.attach_log_stream()
@ -405,6 +362,7 @@ class Service(object):
return [ return [
self.recreate_container( self.recreate_container(
container, container,
do_build=do_build,
timeout=timeout, timeout=timeout,
attach_logs=should_attach_logs, attach_logs=should_attach_logs,
start_new_container=start start_new_container=start
@ -431,7 +389,8 @@ class Service(object):
def recreate_container( def recreate_container(
self, self,
container, container,
timeout=None, do_build=False,
timeout=DEFAULT_TIMEOUT,
attach_logs=False, attach_logs=False,
start_new_container=True): start_new_container=True):
"""Recreate a container. """Recreate a container.
@ -442,9 +401,10 @@ class Service(object):
""" """
log.info("Recreating %s" % container.name) log.info("Recreating %s" % container.name)
container.stop(timeout=self.stop_timeout(timeout)) container.stop(timeout=timeout)
container.rename_to_tmp_name() container.rename_to_tmp_name()
new_container = self.create_container( new_container = self.create_container(
do_build=do_build,
previous_container=container, previous_container=container,
number=container.labels.get(LABEL_CONTAINER_NUMBER), number=container.labels.get(LABEL_CONTAINER_NUMBER),
quiet=True, quiet=True,
@ -456,55 +416,36 @@ class Service(object):
container.remove() container.remove()
return new_container return new_container
def stop_timeout(self, timeout): def start_container_if_stopped(self, container, attach_logs=False):
if timeout is not None:
return timeout
timeout = parse_seconds_float(self.options.get('stop_grace_period'))
if timeout is not None:
return timeout
return DEFAULT_TIMEOUT
def start_container_if_stopped(self, container, attach_logs=False, quiet=False):
if not container.is_running: if not container.is_running:
if not quiet: log.info("Starting %s" % container.name)
log.info("Starting %s" % container.name)
if attach_logs: if attach_logs:
container.attach_log_stream() container.attach_log_stream()
return self.start_container(container) return self.start_container(container)
def start_container(self, container): def start_container(self, container):
self.connect_container_to_networks(container) self.connect_container_to_networks(container)
try: container.start()
container.start()
except APIError as ex:
raise OperationFailedError("Cannot start service %s: %s" % (self.name, ex.explanation))
return container return container
def connect_container_to_networks(self, container): def connect_container_to_networks(self, container):
connected_networks = container.get('NetworkSettings.Networks') connected_networks = container.get('NetworkSettings.Networks')
for network, netdefs in self.networks.items(): for network, aliases in self.networks.items():
if network in connected_networks: if network in connected_networks:
if short_id_alias_exists(container, network):
continue
self.client.disconnect_container_from_network( self.client.disconnect_container_from_network(
container.id, container.id, network)
network)
self.client.connect_container_to_network( self.client.connect_container_to_network(
container.id, network, container.id, network,
aliases=self._get_aliases(netdefs, container), aliases=list(self._get_aliases(container).union(aliases)),
ipv4_address=netdefs.get('ipv4_address', None),
ipv6_address=netdefs.get('ipv6_address', None),
links=self._get_links(False), links=self._get_links(False),
link_local_ips=netdefs.get('link_local_ips', None),
) )
def remove_duplicate_containers(self, timeout=None): def remove_duplicate_containers(self, timeout=DEFAULT_TIMEOUT):
for c in self.duplicate_containers(): for c in self.duplicate_containers():
log.info('Removing %s' % c.name) log.info('Removing %s' % c.name)
c.stop(timeout=self.stop_timeout(timeout)) c.stop(timeout=timeout)
c.remove() c.remove()
def duplicate_containers(self): def duplicate_containers(self):
@ -531,7 +472,7 @@ class Service(object):
'image_id': self.image()['Id'], 'image_id': self.image()['Id'],
'links': self.get_link_names(), 'links': self.get_link_names(),
'net': self.network_mode.id, 'net': self.network_mode.id,
'networks': self.networks, 'networks': list(self.networks.keys()),
'volumes_from': [ 'volumes_from': [
(v.source.name, v.mode) (v.source.name, v.mode)
for v in self.volumes_from if isinstance(v.source, Service) for v in self.volumes_from if isinstance(v.source, Service)
@ -540,38 +481,10 @@ class Service(object):
def get_dependency_names(self): def get_dependency_names(self):
net_name = self.network_mode.service_name net_name = self.network_mode.service_name
return ( return (self.get_linked_service_names() +
self.get_linked_service_names() + self.get_volumes_from_names() +
self.get_volumes_from_names() + ([net_name] if net_name else []) +
([net_name] if net_name else []) + self.options.get('depends_on', []))
list(self.options.get('depends_on', {}).keys())
)
def get_dependency_configs(self):
net_name = self.network_mode.service_name
configs = dict(
[(name, None) for name in self.get_linked_service_names()]
)
configs.update(dict(
[(name, None) for name in self.get_volumes_from_names()]
))
configs.update({net_name: None} if net_name else {})
configs.update(self.options.get('depends_on', {}))
for svc, config in self.options.get('depends_on', {}).items():
if config['condition'] == CONDITION_STARTED:
configs[svc] = lambda s: True
elif config['condition'] == CONDITION_HEALTHY:
configs[svc] = lambda s: s.is_healthy()
else:
# The config schema already prevents this, but it might be
# bypassed if Compose is called programmatically.
raise ValueError(
'depends_on condition "{}" is invalid.'.format(
config['condition']
)
)
return configs
def get_linked_service_names(self): def get_linked_service_names(self):
return [service.name for (service, _) in self.links] return [service.name for (service, _) in self.links]
@ -582,6 +495,10 @@ class Service(object):
def get_volumes_from_names(self): def get_volumes_from_names(self):
return [s.source.name for s in self.volumes_from if isinstance(s.source, Service)] return [s.source.name for s in self.volumes_from if isinstance(s.source, Service)]
def get_container_name(self, number, one_off=False):
# TODO: Implement issue #652 here
return build_container_name(self.project, self.name, number, one_off)
# TODO: this would benefit from github.com/docker/docker/pull/14699 # TODO: this would benefit from github.com/docker/docker/pull/14699
# to remove the need to inspect every container # to remove the need to inspect every container
def _next_container_number(self, one_off=False): def _next_container_number(self, one_off=False):
@ -594,32 +511,11 @@ class Service(object):
numbers = [c.number for c in containers] numbers = [c.number for c in containers]
return 1 if not numbers else max(numbers) + 1 return 1 if not numbers else max(numbers) + 1
def _get_aliases(self, network, container=None): def _get_aliases(self, container):
if container and container.labels.get(LABEL_ONE_OFF) == "True": if container.labels.get(LABEL_ONE_OFF) == "True":
return [] return set()
return list( return {self.name, container.short_id}
{self.name} |
({container.short_id} if container else set()) |
set(network.get('aliases', ()))
)
def build_default_networking_config(self):
if not self.networks:
return {}
network = self.networks[self.network_mode.id]
endpoint = {
'Aliases': self._get_aliases(network),
'IPAMConfig': {},
}
if network.get('ipv4_address'):
endpoint['IPAMConfig']['IPv4Address'] = network.get('ipv4_address')
if network.get('ipv6_address'):
endpoint['IPAMConfig']['IPv6Address'] = network.get('ipv6_address')
return {"EndpointsConfig": {self.network_mode.id: endpoint}}
def _get_links(self, link_to_self): def _get_links(self, link_to_self):
links = {} links = {}
@ -664,10 +560,13 @@ class Service(object):
for k in DOCKER_CONFIG_KEYS if k in self.options) for k in DOCKER_CONFIG_KEYS if k in self.options)
container_options.update(override_options) container_options.update(override_options)
if not container_options.get('name'): if self.custom_container_name() and not one_off:
container_options['name'] = self.custom_container_name()
elif not container_options.get('name'):
container_options['name'] = self.get_container_name(number, one_off) container_options['name'] = self.get_container_name(number, one_off)
container_options.setdefault('detach', True) if 'detach' not in container_options:
container_options['detach'] = True
# If a qualified hostname was given, split it into an # If a qualified hostname was given, split it into an
# unqualified hostname and a domainname unless domainname # unqualified hostname and a domainname unless domainname
@ -681,9 +580,16 @@ class Service(object):
container_options['domainname'] = parts[2] container_options['domainname'] = parts[2]
if 'ports' in container_options or 'expose' in self.options: if 'ports' in container_options or 'expose' in self.options:
container_options['ports'] = build_container_ports( ports = []
container_options, all_ports = container_options.get('ports', []) + self.options.get('expose', [])
self.options) for port_range in all_ports:
internal_range, _ = split_port(port_range)
for port in internal_range:
port = str(port)
if '/' in port:
port = tuple(port.split('/'))
ports.append(port)
container_options['ports'] = ports
container_options['environment'] = merge_environment( container_options['environment'] = merge_environment(
self.options.get('environment'), self.options.get('environment'),
@ -695,14 +601,9 @@ class Service(object):
override_options['binds'] = binds override_options['binds'] = binds
container_options['environment'].update(affinity) container_options['environment'].update(affinity)
container_options['volumes'] = dict( if 'volumes' in container_options:
(v.internal, {}) for v in container_options.get('volumes') or {}) container_options['volumes'] = dict(
(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
@ -720,10 +621,6 @@ class Service(object):
override_options, override_options,
one_off=one_off) one_off=one_off)
networking_config = self.build_default_networking_config()
if networking_config:
container_options['networking_config'] = networking_config
container_options['environment'] = format_environment( container_options['environment'] = format_environment(
container_options['environment']) container_options['environment'])
return container_options return container_options
@ -734,7 +631,7 @@ class Service(object):
logging_dict = options.get('logging', None) logging_dict = options.get('logging', None)
log_config = get_log_config(logging_dict) log_config = get_log_config(logging_dict)
host_config = self.client.create_host_config( return self.client.create_host_config(
links=self._get_links(link_to_self=one_off), links=self._get_links(link_to_self=one_off),
port_bindings=build_port_bindings(options.get('ports') or []), port_bindings=build_port_bindings(options.get('ports') or []),
binds=options.get('binds'), binds=options.get('binds'),
@ -758,38 +655,16 @@ class Service(object):
ipc_mode=options.get('ipc'), ipc_mode=options.get('ipc'),
cgroup_parent=options.get('cgroup_parent'), cgroup_parent=options.get('cgroup_parent'),
cpu_quota=options.get('cpu_quota'), cpu_quota=options.get('cpu_quota'),
shm_size=options.get('shm_size'),
sysctls=options.get('sysctls'),
tmpfs=options.get('tmpfs'),
oom_score_adj=options.get('oom_score_adj'),
mem_swappiness=options.get('mem_swappiness'),
group_add=options.get('group_add'),
userns_mode=options.get('userns_mode')
) )
# TODO: Add as an argument to create_host_config once it's supported
# in docker-py
host_config['Isolation'] = options.get('isolation')
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.stat() doesn't support unicode on some UNIX, so we # python2 os.path() doesn't support unicode, so we need to encode it to
# encode it to a bytestring to be safe # a byte string
if not six.PY3 and not IS_WINDOWS_PLATFORM: if not six.PY3:
path = path.encode('utf8') path = path.encode('utf8')
build_output = self.client.build( build_output = self.client.build(
@ -837,16 +712,9 @@ class Service(object):
'{0}={1}'.format(LABEL_ONE_OFF, "True" if one_off else "False") '{0}={1}'.format(LABEL_ONE_OFF, "True" if one_off else "False")
] ]
@property
def custom_container_name(self): def custom_container_name(self):
return self.options.get('container_name') return self.options.get('container_name')
def get_container_name(self, number, one_off=False):
if self.custom_container_name and not one_off:
return self.custom_container_name
return build_container_name(self.project, self.name, number, one_off)
def remove_image(self, image_type): def remove_image(self, image_type):
if not image_type or image_type == ImageType.none: if not image_type or image_type == ImageType.none:
return False return False
@ -892,58 +760,20 @@ class Service(object):
repo, tag, separator = parse_repository_tag(self.options['image']) repo, tag, separator = parse_repository_tag(self.options['image'])
tag = tag or 'latest' tag = tag or 'latest'
log.info('Pulling %s (%s%s%s)...' % (self.name, repo, separator, tag)) log.info('Pulling %s (%s%s%s)...' % (self.name, repo, separator, tag))
output = self.client.pull(
repo,
tag=tag,
stream=True,
)
try: try:
output = self.client.pull(repo, tag=tag, stream=True) stream_output(output, sys.stdout)
return progress_stream.get_digest_from_pull( except StreamOutputError as e:
stream_output(output, sys.stdout))
except (StreamOutputError, NotFound) as e:
if not ignore_pull_failures: if not ignore_pull_failures:
raise raise
else: else:
log.error(six.text_type(e)) log.error(six.text_type(e))
def push(self, ignore_push_failures=False):
if 'image' not in self.options or 'build' not in self.options:
return
repo, tag, separator = parse_repository_tag(self.options['image'])
tag = tag or 'latest'
log.info('Pushing %s (%s%s%s)...' % (self.name, repo, separator, tag))
output = self.client.push(repo, tag=tag, stream=True)
try:
return progress_stream.get_digest_from_push(
stream_output(output, sys.stdout))
except StreamOutputError as e:
if not ignore_push_failures:
raise
else:
log.error(six.text_type(e))
def is_healthy(self):
""" Check that all containers for this service report healthy.
Returns false if at least one healthcheck is pending.
If an unhealthy container is detected, raise a HealthCheckFailed
exception.
"""
result = True
for ctnr in self.containers():
ctnr.inspect()
status = ctnr.get('State.Health.Status')
if status is None:
raise NoHealthCheckConfigured(self.name)
elif status == 'starting':
result = False
elif status == 'unhealthy':
raise HealthCheckFailed(ctnr.short_id)
return result
def short_id_alias_exists(container, network):
aliases = container.get(
'NetworkSettings.Networks.{net}.Aliases'.format(net=network)) or ()
return container.short_id in aliases
class NetworkMode(object): class NetworkMode(object):
"""A `standard` network mode (ex: host, bridge)""" """A `standard` network mode (ex: host, bridge)"""
@ -1197,22 +1027,5 @@ def format_environment(environment):
def format_env(key, value): def format_env(key, value):
if value is None: if value is None:
return key return key
if isinstance(value, six.binary_type):
value = value.decode('utf-8')
return '{key}={value}'.format(key=key, value=value) return '{key}={value}'.format(key=key, value=value)
return [format_env(*item) for item in environment.items()] return [format_env(*item) for item in environment.items()]
# Ports
def build_container_ports(container_options, options):
ports = []
all_ports = container_options.get('ports', []) + options.get('expose', [])
for port_range in all_ports:
internal_range, _ = split_port(port_range)
for port in internal_range:
port = str(port)
if '/' in port:
port = tuple(port.split('/'))
ports.append(port)
return ports

View file

@ -1,96 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
timeparse.py
(c) Will Roberts <wildwilhelm@gmail.com> 1 February, 2014
This is a vendored and modified copy of:
github.com/wroberts/pytimeparse @ cc0550d
It has been modified to mimic the behaviour of
https://golang.org/pkg/time/#ParseDuration
'''
# MIT LICENSE
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from __future__ import absolute_import
from __future__ import unicode_literals
import re
HOURS = r'(?P<hours>[\d.]+)h'
MINS = r'(?P<mins>[\d.]+)m'
SECS = r'(?P<secs>[\d.]+)s'
MILLI = r'(?P<milli>[\d.]+)ms'
MICRO = r'(?P<micro>[\d.]+)(?:us|µs)'
NANO = r'(?P<nano>[\d.]+)ns'
def opt(x):
return r'(?:{x})?'.format(x=x)
TIMEFORMAT = r'{HOURS}{MINS}{SECS}{MILLI}{MICRO}{NANO}'.format(
HOURS=opt(HOURS),
MINS=opt(MINS),
SECS=opt(SECS),
MILLI=opt(MILLI),
MICRO=opt(MICRO),
NANO=opt(NANO),
)
MULTIPLIERS = dict([
('hours', 60 * 60),
('mins', 60),
('secs', 1),
('milli', 1.0 / 1000),
('micro', 1.0 / 1000.0 / 1000),
('nano', 1.0 / 1000.0 / 1000.0 / 1000.0),
])
def timeparse(sval):
"""Parse a time expression, returning it as a number of seconds. If
possible, the return value will be an `int`; if this is not
possible, the return will be a `float`. Returns `None` if a time
expression cannot be parsed from the given string.
Arguments:
- `sval`: the string value to parse
>>> timeparse('1m24s')
84
>>> timeparse('1.2 minutes')
72
>>> timeparse('1.2 seconds')
1.2
"""
match = re.match(r'\s*' + TIMEFORMAT + r'\s*$', sval, re.I)
if not match or not match.group(0).strip():
return
mdict = match.groupdict()
return sum(
MULTIPLIERS[k] * cast(v) for (k, v) in mdict.items() if v is not None)
def cast(value):
return int(value, 10) if value.isdigit() else float(value)

View file

@ -5,17 +5,11 @@ import codecs
import hashlib import hashlib
import json import json
import json.decoder import json.decoder
import logging
import ntpath
import six import six
from .errors import StreamParseError
from .timeparse import timeparse
json_decoder = json.JSONDecoder() json_decoder = json.JSONDecoder()
log = logging.getLogger(__name__)
def get_output_stream(stream): def get_output_stream(stream):
@ -66,21 +60,13 @@ def split_buffer(stream, splitter=None, decoder=lambda a: a):
yield item yield item
if buffered: if buffered:
try: yield decoder(buffered)
yield decoder(buffered)
except Exception as e:
log.error(
'Compose tried decoding the following data chunk, but failed:'
'\n%s' % repr(buffered)
)
raise StreamParseError(e)
def json_splitter(buffer): def json_splitter(buffer):
"""Attempt to parse a json object from a buffer. If there is at least one """Attempt to parse a json object from a buffer. If there is at least one
object, return it and the rest of the buffer, otherwise return None. object, return it and the rest of the buffer, otherwise return None.
""" """
buffer = buffer.strip()
try: try:
obj, index = json_decoder.raw_decode(buffer) obj, index = json_decoder.raw_decode(buffer)
rest = buffer[json.decoder.WHITESPACE.match(buffer, index).end():] rest = buffer[json.decoder.WHITESPACE.match(buffer, index).end():]
@ -108,28 +94,5 @@ def microseconds_from_time_nano(time_nano):
return int(time_nano % 1000000000 / 1000) return int(time_nano % 1000000000 / 1000)
def nanoseconds_from_time_seconds(time_seconds):
return time_seconds * 1000000000
def parse_seconds_float(value):
return timeparse(value or '')
def parse_nanoseconds_int(value):
parsed = timeparse(value or '')
if parsed is None:
return None
return int(parsed * 1000000000)
def build_string_dict(source_dict): def build_string_dict(source_dict):
return dict((k, str(v if v is not None else '')) for k, v in source_dict.items()) return dict((k, str(v)) for k, v in source_dict.items())
def splitdrive(path):
if len(path) == 0:
return ('', '')
if path[0] in ['.', '\\', '/', '~']:
return ('', path)
return ntpath.splitdrive(path)

View file

@ -3,30 +3,27 @@ from __future__ import unicode_literals
import logging import logging
from docker.errors import APIError
from docker.errors import NotFound from docker.errors import NotFound
from docker.utils import version_lt
from .config import ConfigurationError from .config import ConfigurationError
from .const import LABEL_PROJECT
from .const import LABEL_VOLUME
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class Volume(object): class Volume(object):
def __init__(self, client, project, name, driver=None, driver_opts=None, def __init__(self, client, project, name, driver=None, driver_opts=None,
external_name=None, labels=None): external_name=None):
self.client = client self.client = client
self.project = project self.project = project
self.name = name self.name = name
self.driver = driver self.driver = driver
self.driver_opts = driver_opts self.driver_opts = driver_opts
self.external_name = external_name self.external_name = external_name
self.labels = labels
def create(self): def create(self):
return self.client.create_volume( return self.client.create_volume(
self.full_name, self.driver, self.driver_opts, labels=self._labels self.full_name, self.driver, self.driver_opts
) )
def remove(self): def remove(self):
@ -56,17 +53,6 @@ class Volume(object):
return self.external_name return self.external_name
return '{0}_{1}'.format(self.project, self.name) return '{0}_{1}'.format(self.project, self.name)
@property
def _labels(self):
if version_lt(self.client._version, '1.23'):
return None
labels = self.labels.copy() if self.labels else {}
labels.update({
LABEL_PROJECT: self.project,
LABEL_VOLUME: self.name,
})
return labels
class ProjectVolumes(object): class ProjectVolumes(object):
@ -83,8 +69,7 @@ class ProjectVolumes(object):
name=vol_name, name=vol_name,
driver=data.get('driver'), driver=data.get('driver'),
driver_opts=data.get('driver_opts'), driver_opts=data.get('driver_opts'),
external_name=data.get('external_name'), external_name=data.get('external_name')
labels=data.get('labels')
) )
for vol_name, data in config_volumes.items() for vol_name, data in config_volumes.items()
} }
@ -92,21 +77,17 @@ class ProjectVolumes(object):
def remove(self): def remove(self):
for volume in self.volumes.values(): for volume in self.volumes.values():
try: volume.remove()
volume.remove()
except NotFound:
log.warn("Volume %s not found.", volume.full_name)
def initialize(self): def initialize(self):
try: try:
for volume in self.volumes.values(): for volume in self.volumes.values():
volume_exists = volume.exists()
if volume.external: if volume.external:
log.debug( log.debug(
'Volume {0} declared as external. No new ' 'Volume {0} declared as external. No new '
'volume will be created.'.format(volume.name) 'volume will be created.'.format(volume.name)
) )
if not volume_exists: if not volume.exists():
raise ConfigurationError( raise ConfigurationError(
'Volume {name} declared as external, but could' 'Volume {name} declared as external, but could'
' not be found. Please create the volume manually' ' not be found. Please create the volume manually'
@ -116,32 +97,28 @@ class ProjectVolumes(object):
) )
) )
continue continue
log.info(
if not volume_exists: 'Creating volume "{0}" with {1} driver'.format(
log.info( volume.full_name, volume.driver or 'default'
'Creating volume "{0}" with {1} driver'.format(
volume.full_name, volume.driver or 'default'
)
) )
volume.create() )
else: volume.create()
driver = volume.inspect()['Driver']
if volume.driver is not None and driver != volume.driver:
raise ConfigurationError(
'Configuration for volume {0} specifies driver '
'{1}, but a volume with the same name uses a '
'different driver ({3}). If you wish to use the '
'new configuration, please remove the existing '
'volume "{2}" first:\n'
'$ docker volume rm {2}'.format(
volume.name, volume.driver, volume.full_name,
volume.inspect()['Driver']
)
)
except NotFound: except NotFound:
raise ConfigurationError( raise ConfigurationError(
'Volume %s specifies nonexistent driver %s' % (volume.name, volume.driver) 'Volume %s specifies nonexistent driver %s' % (volume.name, volume.driver)
) )
except APIError as e:
if 'Choose a different volume name' in str(e):
raise ConfigurationError(
'Configuration for volume {0} specifies driver {1}, but '
'a volume with the same name uses a different driver '
'({3}). If you wish to use the new configuration, please '
'remove the existing volume "{2}" first:\n'
'$ docker volume rm {2}'.format(
volume.name, volume.driver, volume.full_name,
volume.inspect()['Driver']
)
)
def namespace_spec(self, volume_spec): def namespace_spec(self, volume_spec):
if not volume_spec.is_named_volume: if not volume_spec.is_named_volume:

View file

@ -18,22 +18,7 @@
__docker_compose_q() { __docker_compose_q() {
docker-compose 2>/dev/null $daemon_options "$@" docker-compose 2>/dev/null ${compose_file:+-f $compose_file} ${compose_project:+-p $compose_project} "$@"
}
# Transforms a multiline list of strings into a single line string
# with the words separated by "|".
__docker_compose_to_alternatives() {
local parts=( $1 )
local IFS='|'
echo "${parts[*]}"
}
# Transforms a multiline list of options into an extglob pattern
# suitable for use in case statements.
__docker_compose_to_extglob() {
local extglob=$( __docker_compose_to_alternatives "$1" )
echo "@($extglob)"
} }
# suppress trailing whitespace # suppress trailing whitespace
@ -42,6 +27,20 @@ __docker_compose_nospace() {
type compopt &>/dev/null && compopt -o nospace type compopt &>/dev/null && compopt -o nospace
} }
# For compatibility reasons, Compose and therefore its completion supports several
# stack compositon files as listed here, in descending priority.
# Support for these filenames might be dropped in some future version.
__docker_compose_compose_file() {
local file
for file in docker-compose.y{,a}ml ; do
[ -e $file ] && {
echo $file
return
}
done
echo docker-compose.yml
}
# Extracts all service names from the compose file. # Extracts all service names from the compose file.
___docker_compose_all_services_in_compose_file() { ___docker_compose_all_services_in_compose_file() {
__docker_compose_q config --services __docker_compose_q config --services
@ -109,18 +108,6 @@ _docker_compose_build() {
} }
_docker_compose_bundle() {
case "$prev" in
--output|-o)
_filedir
return
;;
esac
COMPREPLY=( $( compgen -W "--push-images --help --output -o" -- "$cur" ) )
}
_docker_compose_config() { _docker_compose_config() {
COMPREPLY=( $( compgen -W "--help --quiet -q --services" -- "$cur" ) ) COMPREPLY=( $( compgen -W "--help --quiet -q --services" -- "$cur" ) )
} }
@ -140,22 +127,18 @@ _docker_compose_create() {
_docker_compose_docker_compose() { _docker_compose_docker_compose() {
case "$prev" in case "$prev" in
--tlscacert|--tlscert|--tlskey)
_filedir
return
;;
--file|-f) --file|-f)
_filedir "y?(a)ml" _filedir "y?(a)ml"
return return
;; ;;
$(__docker_compose_to_extglob "$daemon_options_with_args") ) --project-name|-p)
return return
;; ;;
esac esac
case "$cur" in case "$cur" in
-*) -*)
COMPREPLY=( $( compgen -W "$daemon_boolean_options $daemon_options_with_args --help -h --verbose --version -v" -- "$cur" ) ) COMPREPLY=( $( compgen -W "--file -f --help -h --project-name -p --verbose --version -v" -- "$cur" ) )
;; ;;
*) *)
COMPREPLY=( $( compgen -W "${commands[*]}" -- "$cur" ) ) COMPREPLY=( $( compgen -W "${commands[*]}" -- "$cur" ) )
@ -174,7 +157,7 @@ _docker_compose_down() {
case "$cur" in case "$cur" in
-*) -*)
COMPREPLY=( $( compgen -W "--help --rmi --volumes -v --remove-orphans" -- "$cur" ) ) COMPREPLY=( $( compgen -W "--help --rmi --volumes -v" -- "$cur" ) )
;; ;;
esac esac
} }
@ -198,24 +181,6 @@ _docker_compose_events() {
} }
_docker_compose_exec() {
case "$prev" in
--index|--user)
return
;;
esac
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "-d --help --index --privileged -T --user" -- "$cur" ) )
;;
*)
__docker_compose_services_running
;;
esac
}
_docker_compose_help() { _docker_compose_help() {
COMPREPLY=( $( compgen -W "${commands[*]}" -- "$cur" ) ) COMPREPLY=( $( compgen -W "${commands[*]}" -- "$cur" ) )
} }
@ -241,15 +206,9 @@ _docker_compose_kill() {
_docker_compose_logs() { _docker_compose_logs() {
case "$prev" in
--tail)
return
;;
esac
case "$cur" in case "$cur" in
-*) -*)
COMPREPLY=( $( compgen -W "--follow -f --help --no-color --tail --timestamps -t" -- "$cur" ) ) COMPREPLY=( $( compgen -W "--help --no-color" -- "$cur" ) )
;; ;;
*) *)
__docker_compose_services_all __docker_compose_services_all
@ -316,18 +275,6 @@ _docker_compose_pull() {
} }
_docker_compose_push() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--help --ignore-push-failures" -- "$cur" ) )
;;
*)
__docker_compose_services_all
;;
esac
}
_docker_compose_restart() { _docker_compose_restart() {
case "$prev" in case "$prev" in
--timeout|-t) --timeout|-t)
@ -365,14 +312,14 @@ _docker_compose_run() {
__docker_compose_nospace __docker_compose_nospace
return return
;; ;;
--entrypoint|--name|--user|-u|--workdir|-w) --entrypoint|--name|--user|-u)
return return
;; ;;
esac esac
case "$cur" in case "$cur" in
-*) -*)
COMPREPLY=( $( compgen -W "-d --entrypoint -e --help --name --no-deps --publish -p --rm --service-ports -T --user -u --workdir -w" -- "$cur" ) ) COMPREPLY=( $( compgen -W "-d --entrypoint -e --help --name --no-deps --publish -p --rm --service-ports -T --user -u" -- "$cur" ) )
;; ;;
*) *)
__docker_compose_services_all __docker_compose_services_all
@ -434,18 +381,6 @@ _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
-*) -*)
@ -467,7 +402,7 @@ _docker_compose_up() {
case "$cur" in case "$cur" in
-*) -*)
COMPREPLY=( $( compgen -W "--abort-on-container-exit --build -d --force-recreate --help --no-build --no-color --no-deps --no-recreate --timeout -t --remove-orphans" -- "$cur" ) ) COMPREPLY=( $( compgen -W "--abort-on-container-exit -d --force-recreate --help --no-build --no-color --no-deps --no-recreate --timeout -t" -- "$cur" ) )
;; ;;
*) *)
__docker_compose_services_all __docker_compose_services_all
@ -491,12 +426,10 @@ _docker_compose() {
local commands=( local commands=(
build build
bundle
config config
create create
down down
events events
exec
help help
kill kill
logs logs
@ -504,35 +437,17 @@ _docker_compose() {
port port
ps ps
pull pull
push
restart restart
rm rm
run run
scale scale
start start
stop stop
top
unpause unpause
up up
version version
) )
# options for the docker daemon that have to be passed to secondary calls to
# docker-compose executed by this script
local daemon_boolean_options="
--skip-hostname-check
--tls
--tlsverify
"
local daemon_options_with_args="
--file -f
--host -H
--project-name -p
--tlscacert
--tlscert
--tlskey
"
COMPREPLY=() COMPREPLY=()
local cur prev words cword local cur prev words cword
_get_comp_words_by_ref -n : cur prev words cword _get_comp_words_by_ref -n : cur prev words cword
@ -540,19 +455,17 @@ _docker_compose() {
# search subcommand and invoke its handler. # search subcommand and invoke its handler.
# special treatment of some top-level options # special treatment of some top-level options
local command='docker_compose' local command='docker_compose'
local daemon_options=()
local counter=1 local counter=1
local compose_file compose_project
while [ $counter -lt $cword ]; do while [ $counter -lt $cword ]; do
case "${words[$counter]}" in case "${words[$counter]}" in
$(__docker_compose_to_extglob "$daemon_boolean_options") ) --file|-f)
local opt=${words[counter]} (( counter++ ))
daemon_options+=($opt) compose_file="${words[$counter]}"
;; ;;
$(__docker_compose_to_extglob "$daemon_options_with_args") ) --project-name|p)
local opt=${words[counter]} (( counter++ ))
local arg=${words[++counter]} compose_project="${words[$counter]}"
daemon_options+=($opt $arg)
;; ;;
-*) -*)
;; ;;

View file

@ -19,49 +19,52 @@
# * @felixr docker zsh completion script : https://github.com/felixr/docker-zsh-completion # * @felixr docker zsh completion script : https://github.com/felixr/docker-zsh-completion
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
__docker-compose_q() { # For compatibility reasons, Compose and therefore its completion supports several
docker-compose 2>/dev/null $compose_options "$@" # stack compositon files as listed here, in descending priority.
# Support for these filenames might be dropped in some future version.
__docker-compose_compose_file() {
local file
for file in docker-compose.y{,a}ml ; do
[ -e $file ] && {
echo $file
return
}
done
echo docker-compose.yml
} }
# All services defined in docker-compose.yml # Extracts all service names from docker-compose.yml.
__docker-compose_all_services_in_compose_file() { ___docker-compose_all_services_in_compose_file() {
local already_selected local already_selected
local -a services local -a services
already_selected=$(echo $words | tr " " "|") already_selected=$(echo $words | tr " " "|")
__docker-compose_q config --services \ awk -F: '/^[a-zA-Z0-9]/{print $1}' "${compose_file:-$(__docker-compose_compose_file)}" 2>/dev/null | grep -Ev "$already_selected"
| grep -Ev "^(${already_selected})$"
} }
# All services, even those without an existing container # All services, even those without an existing container
__docker-compose_services_all() { __docker-compose_services_all() {
[[ $PREFIX = -* ]] && return 1 [[ $PREFIX = -* ]] && return 1
integer ret=1 integer ret=1
services=$(__docker-compose_all_services_in_compose_file) services=$(___docker-compose_all_services_in_compose_file)
_alternative "args:services:($services)" && ret=0 _alternative "args:services:($services)" && ret=0
return ret return ret
} }
# All services that have an entry with the given key in their docker-compose.yml section # All services that have an entry with the given key in their docker-compose.yml section
__docker-compose_services_with_key() { ___docker-compose_services_with_key() {
local already_selected local already_selected
local -a buildable local -a buildable
already_selected=$(echo $words | tr " " "|") already_selected=$(echo $words | tr " " "|")
# flatten sections to one line, then filter lines containing the key and return section name. # flatten sections to one line, then filter lines containing the key and return section name.
__docker-compose_q config \ awk '/^[a-zA-Z0-9]/{printf "\n"};{printf $0;next;}' "${compose_file:-$(__docker-compose_compose_file)}" 2>/dev/null | awk -F: -v key=": +$1:" '$0 ~ key {print $1}' 2>/dev/null | grep -Ev "$already_selected"
| sed -n -e '/^services:/,/^[^ ]/p' \
| sed -n 's/^ //p' \
| awk '/^[a-zA-Z0-9]/{printf "\n"};{printf $0;next;}' \
| grep " \+$1:" \
| cut -d: -f1 \
| grep -Ev "^(${already_selected})$"
} }
# All services that are defined by a Dockerfile reference # All services that are defined by a Dockerfile reference
__docker-compose_services_from_build() { __docker-compose_services_from_build() {
[[ $PREFIX = -* ]] && return 1 [[ $PREFIX = -* ]] && return 1
integer ret=1 integer ret=1
buildable=$(__docker-compose_services_with_key build) buildable=$(___docker-compose_services_with_key build)
_alternative "args:buildable services:($buildable)" && ret=0 _alternative "args:buildable services:($buildable)" && ret=0
return ret return ret
@ -71,7 +74,7 @@ __docker-compose_services_from_build() {
__docker-compose_services_from_image() { __docker-compose_services_from_image() {
[[ $PREFIX = -* ]] && return 1 [[ $PREFIX = -* ]] && return 1
integer ret=1 integer ret=1
pullable=$(__docker-compose_services_with_key image) pullable=$(___docker-compose_services_with_key image)
_alternative "args:pullable services:($pullable)" && ret=0 _alternative "args:pullable services:($pullable)" && ret=0
return ret return ret
@ -93,7 +96,7 @@ __docker-compose_get_services() {
shift shift
[[ $kind =~ (stopped|all) ]] && args=($args -a) [[ $kind =~ (stopped|all) ]] && args=($args -a)
lines=(${(f)"$(_call_program commands docker $docker_options ps $args)"}) lines=(${(f)"$(_call_program commands docker ps $args)"})
services=(${(f)"$(_call_program commands docker-compose 2>/dev/null $compose_options ps -q)"}) services=(${(f)"$(_call_program commands docker-compose 2>/dev/null $compose_options ps -q)"})
# Parse header line to find columns # Parse header line to find columns
@ -182,17 +185,7 @@ __docker-compose_commands() {
} }
__docker-compose_subcommand() { __docker-compose_subcommand() {
local opts_help opts_force_recreate opts_no_recreate opts_no_build opts_remove_orphans opts_timeout opts_no_color opts_no_deps local opts_help='(: -)--help[Print usage]'
opts_help='(: -)--help[Print usage]'
opts_force_recreate="(--no-recreate)--force-recreate[Recreate containers even if their configuration and image haven't changed. Incompatible with --no-recreate.]"
opts_no_recreate="(--force-recreate)--no-recreate[If containers already exist, don't recreate them. Incompatible with --force-recreate.]"
opts_no_build="(--build)--no-build[Don't build an image, even if it's missing.]"
opts_remove_orphans="--remove-orphans[Remove containers for services not defined in the Compose file]"
opts_timeout=('(-t --timeout)'{-t,--timeout}"[Specify a shutdown timeout in seconds. (default: 10)]:seconds: ")
opts_no_color='--no-color[Produce monochrome output.]'
opts_no_deps="--no-deps[Don't start linked services.]"
integer ret=1 integer ret=1
case "$words[1]" in case "$words[1]" in
@ -200,16 +193,10 @@ __docker-compose_subcommand() {
_arguments \ _arguments \
$opts_help \ $opts_help \
'--force-rm[Always remove intermediate containers.]' \ '--force-rm[Always remove intermediate containers.]' \
'--no-cache[Do not use cache when building the image.]' \ '--no-cache[Do not use cache when building the image]' \
'--pull[Always attempt to pull a newer version of the image.]' \ '--pull[Always attempt to pull a newer version of the image.]' \
'*:services:__docker-compose_services_from_build' && ret=0 '*:services:__docker-compose_services_from_build' && ret=0
;; ;;
(bundle)
_arguments \
$opts_help \
'--push-images[Automatically push images for any services which have a `build` option specified.]' \
'(--output -o)'{--output,-o}'[Path to write the bundle file to. Defaults to "<project name>.dab".]:file:_files' && ret=0
;;
(config) (config)
_arguments \ _arguments \
$opts_help \ $opts_help \
@ -219,37 +206,23 @@ __docker-compose_subcommand() {
(create) (create)
_arguments \ _arguments \
$opts_help \ $opts_help \
$opts_force_recreate \ "(--no-recreate --no-build)--force-recreate[Recreate containers even if their configuration and image haven't changed. Incompatible with --no-recreate.]" \
$opts_no_recreate \ "(--force-recreate)--no-build[If containers already exist, don't recreate them. Incompatible with --force-recreate.]" \
$opts_no_build \ "(--force-recreate)--no-recreate[Don't build an image, even if it's missing]" \
"(--no-build)--build[Build images before creating containers.]" \
'*:services:__docker-compose_services_all' && ret=0 '*:services:__docker-compose_services_all' && ret=0
;; ;;
(down) (down)
_arguments \ _arguments \
$opts_help \ $opts_help \
"--rmi[Remove images. Type must be one of: 'all': Remove all images used by any service. 'local': Remove only images that don't have a custom tag set by the \`image\` field.]:type:(all local)" \ "--rmi[Remove images, type may be one of: 'all' to remove all images, or 'local' to remove only images that don't have an custom name set by the 'image' field]:type:(all local)" \
'(-v --volumes)'{-v,--volumes}"[Remove named volumes declared in the \`volumes\` section of the Compose file and anonymous volumes attached to containers.]" \ '(-v --volumes)'{-v,--volumes}"[Remove data volumes]" && ret=0
$opts_remove_orphans && ret=0
;; ;;
(events) (events)
_arguments \ _arguments \
$opts_help \ $opts_help \
'--json[Output events as a stream of json objects]' \ '--json[Output events as a stream of json objects.]' \
'*:services:__docker-compose_services_all' && ret=0 '*:services:__docker-compose_services_all' && ret=0
;; ;;
(exec)
_arguments \
$opts_help \
'-d[Detached mode: Run command in the background.]' \
'--privileged[Give extended privileges to the process.]' \
'--user=[Run the command as this user.]:username:_users' \
'-T[Disable pseudo-tty allocation. By default `docker-compose exec` allocates a TTY.]' \
'--index=[Index of the container if there are multiple instances of a service \[default: 1\]]:index: ' \
'(-):running services:__docker-compose_runningservices' \
'(-):command: _command_names -e' \
'*::arguments: _normal' && ret=0
;;
(help) (help)
_arguments ':subcommand:__docker-compose_commands' && ret=0 _arguments ':subcommand:__docker-compose_commands' && ret=0
;; ;;
@ -262,10 +235,7 @@ __docker-compose_subcommand() {
(logs) (logs)
_arguments \ _arguments \
$opts_help \ $opts_help \
'(-f --follow)'{-f,--follow}'[Follow log output]' \ '--no-color[Produce monochrome output.]' \
$opts_no_color \
'--tail=[Number of lines to show from the end of the logs for each container.]:number of lines: ' \
'(-t --timestamps)'{-t,--timestamps}'[Show timestamps]' \
'*:services:__docker-compose_services_all' && ret=0 '*:services:__docker-compose_services_all' && ret=0
;; ;;
(pause) (pause)
@ -276,8 +246,8 @@ __docker-compose_subcommand() {
(port) (port)
_arguments \ _arguments \
$opts_help \ $opts_help \
'--protocol=[tcp or udp \[default: tcp\]]:protocol:(tcp udp)' \ '--protocol=-[tcp or udap (defaults to tcp)]:protocol:(tcp udp)' \
'--index=[index of the container if there are multiple instances of a service \[default: 1\]]:index: ' \ '--index=-[index of the container if there are mutiple instances of a service (defaults to 1)]:index: ' \
'1:running services:__docker-compose_runningservices' \ '1:running services:__docker-compose_runningservices' \
'2:port:_ports' && ret=0 '2:port:_ports' && ret=0
;; ;;
@ -293,33 +263,26 @@ __docker-compose_subcommand() {
'--ignore-pull-failures[Pull what it can and ignores images with pull failures.]' \ '--ignore-pull-failures[Pull what it can and ignores images with pull failures.]' \
'*:services:__docker-compose_services_from_image' && ret=0 '*:services:__docker-compose_services_from_image' && ret=0
;; ;;
(push)
_arguments \
$opts_help \
'--ignore-push-failures[Push what it can and ignores images with push failures.]' \
'*:services:__docker-compose_services' && ret=0
;;
(rm) (rm)
_arguments \ _arguments \
$opts_help \ $opts_help \
'(-f --force)'{-f,--force}"[Don't ask to confirm removal]" \ '(-f --force)'{-f,--force}"[Don't ask to confirm removal]" \
'-v[Remove any anonymous volumes attached to containers]' \ '-v[Remove volumes associated with containers]' \
'*:stopped services:__docker-compose_stoppedservices' && ret=0 '*:stopped services:__docker-compose_stoppedservices' && ret=0
;; ;;
(run) (run)
_arguments \ _arguments \
$opts_help \ $opts_help \
'-d[Detached mode: Run container in the background, print new container name.]' \ '-d[Detached mode: Run container in the background, print new container name.]' \
'*-e[KEY=VAL Set an environment variable (can be used multiple times)]:environment variable KEY=VAL: ' \ '--name[Assign a name to the container]:name: ' \
'--entrypoint[Overwrite the entrypoint of the image.]:entry point: ' \ '--entrypoint[Overwrite the entrypoint of the image.]:entry point: ' \
'--name=[Assign a name to the container]:name: ' \ '*-e[KEY=VAL Set an environment variable (can be used multiple times)]:environment variable KEY=VAL: ' \
$opts_no_deps \ '(-u --user)'{-u,--user=-}'[Run as specified username or uid]:username or uid:_users' \
'(-p --publish)'{-p,--publish=}"[Publish a container's port(s) to the host]" \ "--no-deps[Don't start linked services.]" \
'--rm[Remove container after run. Ignored in detached mode.]' \ '--rm[Remove container after run. Ignored in detached mode.]' \
"--service-ports[Run command with the service's ports enabled and mapped to the host.]" \ "--service-ports[Run command with the service's ports enabled and mapped to the host.]" \
'(-p --publish)'{-p,--publish=-}"[Run command with manually mapped container's port(s) to the host.]" \
'-T[Disable pseudo-tty allocation. By default `docker-compose run` allocates a TTY.]' \ '-T[Disable pseudo-tty allocation. By default `docker-compose run` allocates a TTY.]' \
'(-u --user)'{-u,--user=}'[Run as specified username or uid]:username or uid:_users' \
'(-w --workdir)'{-w,--workdir=}'[Working directory inside the container]:workdir: ' \
'(-):services:__docker-compose_services' \ '(-):services:__docker-compose_services' \
'(-):command: _command_names -e' \ '(-):command: _command_names -e' \
'*::arguments: _normal' && ret=0 '*::arguments: _normal' && ret=0
@ -327,7 +290,7 @@ __docker-compose_subcommand() {
(scale) (scale)
_arguments \ _arguments \
$opts_help \ $opts_help \
$opts_timeout \ '(-t --timeout)'{-t,--timeout}"[Specify a shutdown timeout in seconds. (default: 10)]:seconds: " \
'*:running services:__docker-compose_runningservices' && ret=0 '*:running services:__docker-compose_runningservices' && ret=0
;; ;;
(start) (start)
@ -338,12 +301,7 @@ __docker-compose_subcommand() {
(stop|restart) (stop|restart)
_arguments \ _arguments \
$opts_help \ $opts_help \
$opts_timeout \ '(-t --timeout)'{-t,--timeout}"[Specify a shutdown timeout in seconds. (default: 10)]:seconds: " \
'*:running services:__docker-compose_runningservices' && ret=0
;;
(top)
_arguments \
$opts_help \
'*:running services:__docker-compose_runningservices' && ret=0 '*:running services:__docker-compose_runningservices' && ret=0
;; ;;
(unpause) (unpause)
@ -354,16 +312,14 @@ __docker-compose_subcommand() {
(up) (up)
_arguments \ _arguments \
$opts_help \ $opts_help \
'(--abort-on-container-exit)-d[Detached mode: Run containers in the background, print new container names. Incompatible with --abort-on-container-exit.]' \ '(--abort-on-container-exit)-d[Detached mode: Run containers in the background, print new container names.]' \
$opts_no_color \ '--no-color[Produce monochrome output.]' \
$opts_no_deps \ "--no-deps[Don't start linked services.]" \
$opts_force_recreate \ "--force-recreate[Recreate containers even if their configuration and image haven't changed. Incompatible with --no-recreate.]" \
$opts_no_recreate \ "--no-recreate[If containers already exist, don't recreate them.]" \
$opts_no_build \ "--no-build[Don't build an image, even if it's missing]" \
"(--no-build)--build[Build images before starting containers.]" \
"(-d)--abort-on-container-exit[Stops all containers if any container was stopped. Incompatible with -d.]" \ "(-d)--abort-on-container-exit[Stops all containers if any container was stopped. Incompatible with -d.]" \
'(-t --timeout)'{-t,--timeout}"[Use this timeout in seconds for container shutdown when attached or when containers are already running. (default: 10)]:seconds: " \ '(-t --timeout)'{-t,--timeout}"[Specify a shutdown timeout in seconds. (default: 10)]:seconds: " \
$opts_remove_orphans \
'*:services:__docker-compose_services_all' && ret=0 '*:services:__docker-compose_services_all' && ret=0
;; ;;
(version) (version)
@ -391,67 +347,18 @@ _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}"[${file_description}]:file:_files -g '*.yml'" \
'(-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]' \
'(-H --host)'{-H,--host}'[Daemon socket to connect to]:host:' \ '(-f --file)'{-f,--file}'[Specify an alternate docker-compose file (default: docker-compose.yml)]:file:_files -g "*.yml"' \
'--tls[Use TLS; implied by --tlsverify]' \ '(-p --project-name)'{-p,--project-name}'[Specify an alternate project name (default: directory name)]:project name:' \
'--tlscacert=[Trust certs signed only by this CA]:ca path:' \
'--tlscert=[Path to TLS certificate file]:client cert path:' \
'--tlskey=[Path to TLS key file]:tls key path:' \
'--tlsverify[Use TLS and verify the remote]' \
"--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)]" \
'(-): :->command' \ '(-): :->command' \
'(-)*:: :->option-or-argument' && ret=0 '(-)*:: :->option-or-argument' && ret=0
local -a relevant_compose_flags relevant_docker_flags compose_options docker_options local compose_file=${opt_args[-f]}${opt_args[--file]}
local compose_project=${opt_args[-p]}${opt_args[--project-name]}
relevant_compose_flags=( local compose_options="${compose_file:+--file $compose_file} ${compose_project:+--project-name $compose_project}"
"--file" "-f"
"--host" "-H"
"--project-name" "-p"
"--tls"
"--tlscacert"
"--tlscert"
"--tlskey"
"--tlsverify"
"--skip-hostname-check"
)
relevant_docker_flags=(
"--host" "-H"
"--tls"
"--tlscacert"
"--tlscert"
"--tlskey"
"--tlsverify"
)
for k in "${(@k)opt_args}"; do
if [[ -n "${relevant_docker_flags[(r)$k]}" ]]; then
docker_options+=$k
if [[ -n "$opt_args[$k]" ]]; then
docker_options+=$opt_args[$k]
fi
fi
if [[ -n "${relevant_compose_flags[(r)$k]}" ]]; then
compose_options+=$k
if [[ -n "$opt_args[$k]" ]]; then
compose_options+=$opt_args[$k]
fi
fi
done
case $state in case $state in
(command) (command)

View file

@ -18,28 +18,23 @@ exe = EXE(pyz,
a.datas, a.datas,
[ [
( (
'compose/config/config_schema_v1.json', 'compose/config/fields_schema_v1.json',
'compose/config/config_schema_v1.json', 'compose/config/fields_schema_v1.json',
'DATA' 'DATA'
), ),
( (
'compose/config/config_schema_v2.0.json', 'compose/config/fields_schema_v2.0.json',
'compose/config/config_schema_v2.0.json', 'compose/config/fields_schema_v2.0.json',
'DATA' 'DATA'
), ),
( (
'compose/config/config_schema_v2.1.json', 'compose/config/service_schema_v1.json',
'compose/config/config_schema_v2.1.json', 'compose/config/service_schema_v1.json',
'DATA' 'DATA'
), ),
( (
'compose/config/config_schema_v3.0.json', 'compose/config/service_schema_v2.0.json',
'compose/config/config_schema_v3.0.json', 'compose/config/service_schema_v2.0.json',
'DATA'
),
(
'compose/config/config_schema_v3.1.json',
'compose/config/config_schema_v3.1.json',
'DATA' 'DATA'
), ),
( (

17
docs/Dockerfile Normal file
View file

@ -0,0 +1,17 @@
FROM docs/base:latest
MAINTAINER Mary Anthony <mary@docker.com> (@moxiegirl)
RUN svn checkout https://github.com/docker/docker/trunk/docs /docs/content/engine
RUN svn checkout https://github.com/docker/swarm/trunk/docs /docs/content/swarm
RUN svn checkout https://github.com/docker/machine/trunk/docs /docs/content/machine
RUN svn checkout https://github.com/docker/distribution/trunk/docs /docs/content/registry
RUN svn checkout https://github.com/docker/notary/trunk/docs /docs/content/notary
RUN svn checkout https://github.com/docker/kitematic/trunk/docs /docs/content/kitematic
RUN svn checkout https://github.com/docker/toolbox/trunk/docs /docs/content/toolbox
RUN svn checkout https://github.com/docker/opensource/trunk/docs /docs/content/project
ENV PROJECT=compose
# To get the git info for this repo
COPY . /src
COPY . /docs/content/$PROJECT/

55
docs/Makefile Normal file
View file

@ -0,0 +1,55 @@
.PHONY: all binary build cross default docs docs-build docs-shell shell test test-unit test-integration test-integration-cli test-docker-py validate
# env vars passed through directly to Docker's build scripts
# to allow things like `make DOCKER_CLIENTONLY=1 binary` easily
# `docs/sources/contributing/devenvironment.md ` and `project/PACKAGERS.md` have some limited documentation of some of these
DOCKER_ENVS := \
-e BUILDFLAGS \
-e DOCKER_CLIENTONLY \
-e DOCKER_EXECDRIVER \
-e DOCKER_GRAPHDRIVER \
-e TESTDIRS \
-e TESTFLAGS \
-e TIMEOUT
# note: we _cannot_ add "-e DOCKER_BUILDTAGS" here because even if it's unset in the shell, that would shadow the "ENV DOCKER_BUILDTAGS" set in our Dockerfile, which is very important for our official builds
# to allow `make DOCSDIR=1 docs-shell` (to create a bind mount in docs)
DOCS_MOUNT := $(if $(DOCSDIR),-v $(CURDIR):/docs/content/compose)
# to allow `make DOCSPORT=9000 docs`
DOCSPORT := 8000
# Get the IP ADDRESS
DOCKER_IP=$(shell python -c "import urlparse ; print urlparse.urlparse('$(DOCKER_HOST)').hostname or ''")
HUGO_BASE_URL=$(shell test -z "$(DOCKER_IP)" && echo localhost || echo "$(DOCKER_IP)")
HUGO_BIND_IP=0.0.0.0
GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null)
DOCKER_IMAGE := docker$(if $(GIT_BRANCH),:$(GIT_BRANCH))
DOCKER_DOCS_IMAGE := docs-base$(if $(GIT_BRANCH),:$(GIT_BRANCH))
DOCKER_RUN_DOCS := docker run --rm -it $(DOCS_MOUNT) -e AWS_S3_BUCKET -e NOCACHE
# for some docs workarounds (see below in "docs-build" target)
GITCOMMIT := $(shell git rev-parse --short HEAD 2>/dev/null)
default: docs
docs: docs-build
$(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" hugo server --port=$(DOCSPORT) --baseUrl=$(HUGO_BASE_URL) --bind=$(HUGO_BIND_IP) --watch
docs-draft: docs-build
$(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" hugo server --buildDrafts="true" --port=$(DOCSPORT) --baseUrl=$(HUGO_BASE_URL) --bind=$(HUGO_BIND_IP)
docs-shell: docs-build
$(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 "$(DOCKER_DOCS_IMAGE)" bash
docs-build:
# ( git remote | grep -v upstream ) || git diff --name-status upstream/release..upstream/docs ./ > ./changed-files
# echo "$(GIT_BRANCH)" > GIT_BRANCH
# echo "$(AWS_S3_BUCKET)" > AWS_S3_BUCKET
# echo "$(GITCOMMIT)" > GITCOMMIT
docker build -t "$(DOCKER_DOCS_IMAGE)" .

View file

@ -1,16 +1,86 @@
# The docs have been moved! <!--[metadata]>
+++
draft = true
title = "Compose README"
description = "Compose README"
keywords = ["Docker, documentation, manual, guide, reference, api"]
+++
<![end-metadata]-->
The documentation for Compose has been merged into # Contributing to the Docker Compose documentation
[the general documentation repo](https://github.com/docker/docker.github.io).
The docs for Compose are now here: The documentation in this directory is part of the [https://docs.docker.com](https://docs.docker.com) website. Docker uses [the Hugo static generator](http://gohugo.io/overview/introduction/) to convert project Markdown files to a static HTML site.
https://github.com/docker/docker.github.io/tree/master/compose
Please submit pull requests for unpublished features on the `vnext-compose` branch (https://github.com/docker/docker.github.io/tree/vnext-compose). You don't need to be a Hugo expert to contribute to the compose documentation. If you are familiar with Markdown, you can modify the content in the `docs` files.
If you submit a PR to this codebase that has a docs impact, create a second docs PR on `docker.github.io`. Use the docs PR template provided (coming soon - watch this space). If you want to add a new file or change the location of the document in the menu, you do need to know a little more.
PRs for typos, additional information, etc. for already-published features should be labeled as `okay-to-publish` (we are still settling on a naming convention, will provide a label soon). You can submit these PRs either to `vnext-compose` or directly to `master` on `docker.github.io` ## Documentation contributing workflow
As always, the docs remain open-source and we appreciate your feedback and 1. Edit a Markdown file in the tree.
pull requests!
2. Save your changes.
3. Make sure you are in the `docs` subdirectory.
4. Build the documentation.
$ make docs
---> ffcf3f6c4e97
Removing intermediate container a676414185e8
Successfully built ffcf3f6c4e97
docker run --rm -it -e AWS_S3_BUCKET -e NOCACHE -p 8000:8000 -e DOCKERHOST "docs-base:test-tooling" hugo server --port=8000 --baseUrl=192.168.59.103 --bind=0.0.0.0
ERROR: 2015/06/13 MenuEntry's .Url is deprecated and will be removed in Hugo 0.15. Use .URL instead.
0 of 4 drafts rendered
0 future content
12 pages created
0 paginator pages created
0 tags created
0 categories created
in 55 ms
Serving pages from /docs/public
Web Server is available at http://0.0.0.0:8000/
Press Ctrl+C to stop
5. Open the available server in your browser.
The documentation server has the complete menu but only the Docker Compose
documentation resolves. You can't access the other project docs from this
localized build.
## Tips on Hugo metadata and menu positioning
The top of each Docker Compose documentation file contains TOML metadata. The metadata is commented out to prevent it from appearing in GitHub.
<!--[metadata]>
+++
title = "Extending services in Compose"
description = "How to use Docker Compose's extends keyword to share configuration between files and projects"
keywords = ["fig, composition, compose, docker, orchestration, documentation, docs"]
[menu.main]
parent="workw_compose"
weight=2
+++
<![end-metadata]-->
The metadata alone has this structure:
+++
title = "Extending services in Compose"
description = "How to use Docker Compose's extends keyword to share configuration between files and projects"
keywords = ["fig, composition, compose, docker, orchestration, documentation, docs"]
[menu.main]
parent="workw_compose"
weight=2
+++
The `[menu.main]` section refers to navigation defined [in the main Docker menu](https://github.com/docker/docs-base/blob/hugo/config.toml). This metadata says *add a menu item called* Extending services in Compose *to the menu with the* `smn_workdw_compose` *identifier*. If you locate the menu in the configuration, you'll find *Create multi-container applications* is the menu title.
You can move an article in the tree by specifying a new parent. You can shift the location of the item by changing its weight. Higher numbers are heavier and shift the item to the bottom of menu. Low or no numbers shift it up.
## Other key documentation repositories
The `docker/docs-base` repository contains [the Hugo theme and menu configuration](https://github.com/docker/docs-base). If you open the `Dockerfile` you'll see the `make docs` relies on this as a base image for building the Compose documentation.
The `docker/docs.docker.com` repository contains [build system for building the Docker documentation site](https://github.com/docker/docs.docker.com). Fork this repository to build the entire documentation site.

68
docs/completion.md Normal file
View file

@ -0,0 +1,68 @@
<!--[metadata]>
+++
title = "Command-line Completion"
description = "Compose CLI reference"
keywords = ["fig, composition, compose, docker, orchestration, cli, reference"]
[menu.main]
parent="workw_compose"
weight=88
+++
<![end-metadata]-->
# Command-line Completion
Compose comes with [command completion](http://en.wikipedia.org/wiki/Command-line_completion)
for the bash and zsh shell.
## Installing Command Completion
### Bash
Make sure bash completion is installed. If you use a current Linux in a non-minimal installation, bash completion should be available.
On a Mac, install with `brew install bash-completion`
Place the completion script in `/etc/bash_completion.d/` (`/usr/local/etc/bash_completion.d/` on a Mac), using e.g.
curl -L https://raw.githubusercontent.com/docker/compose/$(docker-compose version --short)/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose
Completion will be available upon next login.
### Zsh
Place the completion script in your `/path/to/zsh/completion`, using e.g. `~/.zsh/completion/`
mkdir -p ~/.zsh/completion
curl -L https://raw.githubusercontent.com/docker/compose/$(docker-compose version --short)/contrib/completion/zsh/_docker-compose > ~/.zsh/completion/_docker-compose
Include the directory in your `$fpath`, e.g. by adding in `~/.zshrc`
fpath=(~/.zsh/completion $fpath)
Make sure `compinit` is loaded or do it by adding in `~/.zshrc`
autoload -Uz compinit && compinit -i
Then reload your shell
exec $SHELL -l
## Available completions
Depending on what you typed on the command line so far, it will complete
- available docker-compose commands
- options that are available for a particular command
- service names that make sense in a given context (e.g. services with running or stopped instances or services based on images vs. services based on Dockerfiles). For `docker-compose scale`, completed service names will automatically have "=" appended.
- arguments for selected options, e.g. `docker-compose kill -s` will complete some signals like SIGHUP and SIGUSR1.
Enjoy working with Compose faster and with less typos!
## Compose documentation
- [User guide](index.md)
- [Installing Compose](install.md)
- [Get started with Django](django.md)
- [Get started with Rails](rails.md)
- [Get started with WordPress](wordpress.md)
- [Command line reference](./reference/index.md)
- [Compose file reference](compose-file.md)

1067
docs/compose-file.md Normal file

File diff suppressed because it is too large Load diff

182
docs/django.md Normal file
View file

@ -0,0 +1,182 @@
<!--[metadata]>
+++
title = "Quickstart: Compose and Django"
description = "Getting started with Docker Compose and Django"
keywords = ["documentation, docs, docker, compose, orchestration, containers"]
[menu.main]
parent="workw_compose"
weight=4
+++
<![end-metadata]-->
# Quickstart: Compose and Django
This quick-start guide demonstrates how to use Compose to set up and run a
simple Django/PostgreSQL app. Before starting, you'll need to have
[Compose installed](install.md).
## Define the project components
For this project, you need to create a Dockerfile, a Python dependencies file,
and a `docker-compose.yml` file.
1. Create an empty project directory.
You can name the directory something easy for you to remember. This directory is the context for your application image. The directory should only contain resources to build that image.
2. Create a new file called `Dockerfile` in your project directory.
The Dockerfile defines an application's image content via one or more build
commands that configure that image. Once built, you can run the image in a
container. For more information on `Dockerfiles`, see the [Docker user
guide](https://docs.docker.com/engine/userguide/dockerimages/#building-an-image-from-a-dockerfile)
and the [Dockerfile reference](https://docs.docker.com/engine/reference/builder/).
3. Add the following content to the `Dockerfile`.
FROM python:2.7
ENV PYTHONUNBUFFERED 1
RUN mkdir /code
WORKDIR /code
ADD requirements.txt /code/
RUN pip install -r requirements.txt
ADD . /code/
This `Dockerfile` starts with a Python 2.7 base image. The base image is
modified by adding a new `code` directory. The base image is further modified
by installing the Python requirements defined in the `requirements.txt` file.
4. Save and close the `Dockerfile`.
5. Create a `requirements.txt` in your project directory.
This file is used by the `RUN pip install -r requirements.txt` command in your `Dockerfile`.
6. Add the required software in the file.
Django
psycopg2
7. Save and close the `requirements.txt` file.
8. Create a file called `docker-compose.yml` in your project directory.
The `docker-compose.yml` file describes the services that make your app. In
this example those services are a web server and database. The compose file
also describes which Docker images these services use, how they link
together, any volumes they might need mounted inside the containers.
Finally, the `docker-compose.yml` file describes which ports these services
expose. See the [`docker-compose.yml` reference](compose-file.md) for more
information on how this file works.
9. Add the following configuration to the file.
version: '2'
services:
db:
image: postgres
web:
build: .
command: python manage.py runserver 0.0.0.0:8000
volumes:
- .:/code
ports:
- "8000:8000"
depends_on:
- db
This file defines two services: The `db` service and the `web` service.
10. Save and close the `docker-compose.yml` file.
## Create a Django project
In this step, you create a Django started project by building the image from the build context defined in the previous procedure.
1. Change to the root of your project directory.
2. Create the Django project using the `docker-compose` command.
$ docker-compose run web django-admin.py startproject composeexample .
This instructs Compose to run `django-admin.py startproject composeeexample`
in a container, using the `web` service's image and configuration. Because
the `web` image doesn't exist yet, Compose builds it from the current
directory, as specified by the `build: .` line in `docker-compose.yml`.
Once the `web` service image is built, Compose runs it and executes the
`django-admin.py startproject` command in the container. This command
instructs Django to create a set of files and directories representing a
Django project.
3. After the `docker-compose` command completes, list the contents of your project.
$ ls -l
drwxr-xr-x 2 root root composeexample
-rw-rw-r-- 1 user user docker-compose.yml
-rw-rw-r-- 1 user user Dockerfile
-rwxr-xr-x 1 root root manage.py
-rw-rw-r-- 1 user user requirements.txt
The files `django-admin` created are owned by root. This happens because
the container runs as the `root` user.
4. Change the ownership of the new files.
sudo chown -R $USER:$USER .
## Connect the database
In this section, you set up the database connection for Django.
1. In your project directory, edit the `composeexample/settings.py` file.
2. Replace the `DATABASES = ...` with the following:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'postgres',
'USER': 'postgres',
'HOST': 'db',
'PORT': 5432,
}
}
These settings are determined by the
[postgres](https://hub.docker.com/_/postgres/) Docker image
specified in `docker-compose.yml`.
3. Save and close the file.
4. Run the `docker-compose up` command.
$ docker-compose up
Starting composepractice_db_1...
Starting composepractice_web_1...
Attaching to composepractice_db_1, composepractice_web_1
...
db_1 | PostgreSQL init process complete; ready for start up.
...
db_1 | LOG: database system is ready to accept connections
db_1 | LOG: autovacuum launcher started
..
web_1 | Django version 1.8.4, using settings 'composeexample.settings'
web_1 | Starting development server at http://0.0.0.0:8000/
web_1 | Quit the server with CONTROL-C.
At this point, your Django app should be running at port `8000` on your
Docker host. If you are using a Docker Machine VM, you can use the
`docker-machine ip MACHINE_NAME` to get the IP address.
## More Compose documentation
- [User guide](index.md)
- [Installing Compose](install.md)
- [Getting Started](gettingstarted.md)
- [Get started with Rails](rails.md)
- [Get started with WordPress](wordpress.md)
- [Command line reference](./reference/index.md)
- [Compose file reference](compose-file.md)

368
docs/extends.md Normal file
View file

@ -0,0 +1,368 @@
<!--[metadata]>
+++
title = "Extending Services in Compose"
description = "How to use Docker Compose's extends keyword to share configuration between files and projects"
keywords = ["fig, composition, compose, docker, orchestration, documentation, docs"]
[menu.main]
parent="workw_compose"
weight=20
+++
<![end-metadata]-->
# Extending services and Compose files
Compose supports two methods of sharing common configuration:
1. Extending an entire Compose file by
[using multiple Compose files](#multiple-compose-files)
2. Extending individual services with [the `extends` field](#extending-services)
## Multiple Compose files
Using multiple Compose files enables you to customize a Compose application
for different environments or different workflows.
### Understanding multiple Compose files
By default, Compose reads two files, a `docker-compose.yml` and an optional
`docker-compose.override.yml` file. By convention, the `docker-compose.yml`
contains your base configuration. The override file, as its name implies, can
contain configuration overrides for existing services or entirely new
services.
If a service is defined in both files Compose merges the configurations using
the rules described in [Adding and overriding
configuration](#adding-and-overriding-configuration).
To use multiple override files, or an override file with a different name, you
can use the `-f` option to specify the list of files. Compose merges files in
the order they're specified on the command line. See the [`docker-compose`
command reference](./reference/overview.md) for more information about
using `-f`.
When you use multiple configuration files, you must make sure all paths in the
files are relative to the base Compose file (the first Compose file specified
with `-f`). This is required because override files need not be valid
Compose files. Override files can contain small fragments of configuration.
Tracking which fragment of a service is relative to which path is difficult and
confusing, so to keep paths easier to understand, all paths must be defined
relative to the base file.
### Example use case
In this section are two common use cases for multiple compose files: changing a
Compose app for different environments, and running administrative tasks
against a Compose app.
#### Different environments
A common use case for multiple files is changing a development Compose app
for a production-like environment (which may be production, staging or CI).
To support these differences, you can split your Compose configuration into
a few different files:
Start with a base file that defines the canonical configuration for the
services.
**docker-compose.yml**
web:
image: example/my_web_app:latest
links:
- db
- cache
db:
image: postgres:latest
cache:
image: redis:latest
In this example the development configuration exposes some ports to the
host, mounts our code as a volume, and builds the web image.
**docker-compose.override.yml**
web:
build: .
volumes:
- '.:/code'
ports:
- 8883:80
environment:
DEBUG: 'true'
db:
command: '-d'
ports:
- 5432:5432
cache:
ports:
- 6379:6379
When you run `docker-compose up` it reads the overrides automatically.
Now, it would be nice to use this Compose app in a production environment. So,
create another override file (which might be stored in a different git
repo or managed by a different team).
**docker-compose.prod.yml**
web:
ports:
- 80:80
environment:
PRODUCTION: 'true'
cache:
environment:
TTL: '500'
To deploy with this production Compose file you can run
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
This deploys all three services using the configuration in
`docker-compose.yml` and `docker-compose.prod.yml` (but not the
dev configuration in `docker-compose.override.yml`).
See [production](production.md) for more information about Compose in
production.
#### Administrative tasks
Another common use case is running adhoc or administrative tasks against one
or more services in a Compose app. This example demonstrates running a
database backup.
Start with a **docker-compose.yml**.
web:
image: example/my_web_app:latest
links:
- db
db:
image: postgres:latest
In a **docker-compose.admin.yml** add a new service to run the database
export or backup.
dbadmin:
build: database_admin/
links:
- db
To start a normal environment run `docker-compose up -d`. To run a database
backup, include the `docker-compose.admin.yml` as well.
docker-compose -f docker-compose.yml -f docker-compose.admin.yml \
run dbadmin db-backup
## Extending services
Docker Compose's `extends` keyword enables sharing of common configurations
among different files, or even different projects entirely. Extending services
is useful if you have several services that reuse a common set of configuration
options. Using `extends` you can define a common set of service options in one
place and refer to it from anywhere.
> **Note:** `links`, `volumes_from`, and `depends_on` are never shared between
> services using >`extends`. These exceptions exist to avoid
> implicit dependencies&mdash;you always define `links` and `volumes_from`
> locally. This ensures dependencies between services are clearly visible when
> reading the current file. Defining these locally also ensures changes to the
> referenced file don't result in breakage.
### Understand the extends configuration
When defining any service in `docker-compose.yml`, you can declare that you are
extending another service like this:
web:
extends:
file: common-services.yml
service: webapp
This instructs Compose to re-use the configuration for the `webapp` service
defined in the `common-services.yml` file. Suppose that `common-services.yml`
looks like this:
webapp:
build: .
ports:
- "8000:8000"
volumes:
- "/data"
In this case, you'll get exactly the same result as if you wrote
`docker-compose.yml` with the same `build`, `ports` and `volumes` configuration
values defined directly under `web`.
You can go further and define (or re-define) configuration locally in
`docker-compose.yml`:
web:
extends:
file: common-services.yml
service: webapp
environment:
- DEBUG=1
cpu_shares: 5
important_web:
extends: web
cpu_shares: 10
You can also write other services and link your `web` service to them:
web:
extends:
file: common-services.yml
service: webapp
environment:
- DEBUG=1
cpu_shares: 5
links:
- db
db:
image: postgres
### Example use case
Extending an individual service is useful when you have multiple services that
have a common configuration. The example below is a Compose app with
two services: a web application and a queue worker. Both services use the same
codebase and share many configuration options.
In a **common.yml** we define the common configuration:
app:
build: .
environment:
CONFIG_FILE_PATH: /code/config
API_KEY: xxxyyy
cpu_shares: 5
In a **docker-compose.yml** we define the concrete services which use the
common configuration:
webapp:
extends:
file: common.yml
service: app
command: /code/run_web_app
ports:
- 8080:8080
links:
- queue
- db
queue_worker:
extends:
file: common.yml
service: app
command: /code/run_worker
links:
- queue
## Adding and overriding configuration
Compose copies configurations from the original service over to the local one.
If a configuration option is defined in both the original service the local
service, the local value *replaces* or *extends* the original value.
For single-value options like `image`, `command` or `mem_limit`, the new value
replaces the old value.
# original service
command: python app.py
# local service
command: python otherapp.py
# result
command: python otherapp.py
In the case of `build` and `image`, using one in the local service causes
Compose to discard the other, if it was defined in the original service.
Example of image replacing build:
# original service
build: .
# local service
image: redis
# result
image: redis
Example of build replacing image:
# original service
image: redis
# local service
build: .
# result
build: .
For the **multi-value options** `ports`, `expose`, `external_links`, `dns` and
`dns_search`, Compose concatenates both sets of values:
# original service
expose:
- "3000"
# local service
expose:
- "4000"
- "5000"
# result
expose:
- "3000"
- "4000"
- "5000"
In the case of `environment`, `labels`, `volumes` and `devices`, Compose
"merges" entries together with locally-defined values taking precedence:
# original service
environment:
- FOO=original
- BAR=original
# local service
environment:
- BAR=local
- BAZ=local
# result
environment:
- FOO=original
- BAR=local
- BAZ=local
## Compose documentation
- [User guide](index.md)
- [Installing Compose](install.md)
- [Getting Started](gettingstarted.md)
- [Get started with Django](django.md)
- [Get started with Rails](rails.md)
- [Get started with WordPress](wordpress.md)
- [Command line reference](./reference/index.md)
- [Compose file reference](compose-file.md)

140
docs/faq.md Normal file
View file

@ -0,0 +1,140 @@
<!--[metadata]>
+++
title = "Frequently Asked Questions"
description = "Docker Compose FAQ"
keywords = "documentation, docs, docker, compose, faq"
[menu.main]
identifier="faq.compose"
parent="workw_compose"
weight=90
+++
<![end-metadata]-->
# Frequently asked questions
If you dont see your question here, feel free to drop by `#docker-compose` on
freenode IRC and ask the community.
## Why do my services take 10 seconds to stop?
Compose stop attempts to stop a container by sending a `SIGTERM`. It then waits
for a [default timeout of 10 seconds](./reference/stop.md). After the timeout,
a `SIGKILL` is sent to the container to forcefully kill it. If you
are waiting for this timeout, it means that your containers aren't shutting down
when they receive the `SIGTERM` signal.
There has already been a lot written about this problem of
[processes handling signals](https://medium.com/@gchudnov/trapping-signals-in-docker-containers-7a57fdda7d86)
in containers.
To fix this problem, try the following:
* Make sure you're using the JSON form of `CMD` and `ENTRYPOINT`
in your Dockerfile.
For example use `["program", "arg1", "arg2"]` not `"program arg1 arg2"`.
Using the string form causes Docker to run your process using `bash` which
doesn't handle signals properly. Compose always uses the JSON form, so don't
worry if you override the command or entrypoint in your Compose file.
* If you are able, modify the application that you're running to
add an explicit signal handler for `SIGTERM`.
* If you can't modify the application, wrap the application in a lightweight init
system (like [s6](http://skarnet.org/software/s6/)) or a signal proxy (like
[dumb-init](https://github.com/Yelp/dumb-init) or
[tini](https://github.com/krallin/tini)). Either of these wrappers take care of
handling `SIGTERM` properly.
## How do I run multiple copies of a Compose file on the same host?
Compose uses the project name to create unique identifiers for all of a
project's containers and other resources. To run multiple copies of a project,
set a custom project name using the [`-p` command line
option](./reference/overview.md) or the [`COMPOSE_PROJECT_NAME`
environment variable](./reference/envvars.md#compose-project-name).
## What's the difference between `up`, `run`, and `start`?
Typically, you want `docker-compose up`. Use `up` to start or restart all the
services defined in a `docker-compose.yml`. In the default "attached"
mode, you'll see all the logs from all the containers. In "detached" mode (`-d`),
Compose exits after starting the containers, but the containers continue to run
in the background.
The `docker-compose run` command is for running "one-off" or "adhoc" tasks. It
requires the service name you want to run and only starts containers for services
that the running service depends on. Use `run` to run tests or perform
an administrative task such as removing or adding data to a data volume
container. The `run` command acts like `docker run -ti` in that it opens an
interactive terminal to the container and returns an exit status matching the
exit status of the process in the container.
The `docker-compose start` command is useful only to restart containers
that were previously created, but were stopped. It never creates new
containers.
## Can I use json instead of yaml for my Compose file?
Yes. [Yaml is a superset of json](http://stackoverflow.com/a/1729545/444646) so
any JSON file should be valid Yaml. To use a JSON file with Compose,
specify the filename to use, for example:
```bash
docker-compose -f docker-compose.json up
```
## How do I get Compose to wait for my database to be ready before starting my application?
Unfortunately, Compose won't do that for you but for a good reason.
The problem of waiting for a database to be ready is really just a subset of a
much larger problem of distributed systems. In production, your database could
become unavailable or move hosts at any time. The application needs to be
resilient to these types of failures.
To handle this, the application would attempt to re-establish a connection to
the database after a failure. If the application retries the connection,
it should eventually be able to connect to the database.
To wait for the application to be in a good state, you can implement a
healthcheck. A healthcheck makes a request to the application and checks
the response for a success status code. If it is not successful it waits
for a short period of time, and tries again. After some timeout value, the check
stops trying and report a failure.
If you need to run tests against your application, you can start by running a
healthcheck. Once the healthcheck gets a successful response, you can start
running your tests.
## Should I include my code with `COPY`/`ADD` or a volume?
You can add your code to the image using `COPY` or `ADD` directive in a
`Dockerfile`. This is useful if you need to relocate your code along with the
Docker image, for example when you're sending code to another environment
(production, CI, etc).
You should use a `volume` if you want to make changes to your code and see them
reflected immediately, for example when you're developing code and your server
supports hot code reloading or live-reload.
There may be cases where you'll want to use both. You can have the image
include the code using a `COPY`, and use a `volume` in your Compose file to
include the code from the host during development. The volume overrides
the directory contents of the image.
## Where can I find example compose files?
There are [many examples of Compose files on
github](https://github.com/search?q=in%3Apath+docker-compose.yml+extension%3Ayml&type=Code).
## Compose documentation
- [Installing Compose](install.md)
- [Get started with Django](django.md)
- [Get started with Rails](rails.md)
- [Get started with WordPress](wordpress.md)
- [Command line reference](./reference/index.md)
- [Compose file reference](compose-file.md)

191
docs/gettingstarted.md Normal file
View file

@ -0,0 +1,191 @@
<!--[metadata]>
+++
title = "Getting Started"
description = "Getting started with Docker Compose"
keywords = ["documentation, docs, docker, compose, orchestration, containers"]
[menu.main]
parent="workw_compose"
weight=-85
+++
<![end-metadata]-->
# Getting Started
On this page you build a simple Python web application running on Compose. The
application uses the Flask framework and increments a value in Redis. While the
sample uses Python, the concepts demonstrated here should be understandable even
if you're not familiar with it.
## Prerequisites
Make sure you have already
[installed both Docker Engine and Docker Compose](install.md). You
don't need to install Python, it is provided by a Docker image.
## Step 1: Setup
1. Create a directory for the project:
$ mkdir composetest
$ cd composetest
2. With your favorite text editor create a file called `app.py` in your project
directory.
from flask import Flask
from redis import Redis
app = Flask(__name__)
redis = Redis(host='redis', port=6379)
@app.route('/')
def hello():
redis.incr('hits')
return 'Hello World! I have been seen %s times.' % redis.get('hits')
if __name__ == "__main__":
app.run(host="0.0.0.0", debug=True)
3. Create another file called `requirements.txt` in your project directory and
add the following:
flask
redis
These define the applications dependencies.
## Step 2: Create a Docker image
In this step, you build a new Docker image. The image contains all the
dependencies the Python application requires, including Python itself.
1. In your project directory create a file named `Dockerfile` and add the
following:
FROM python:2.7
ADD . /code
WORKDIR /code
RUN pip install -r requirements.txt
CMD python app.py
This tells Docker to:
* Build an image starting with the Python 2.7 image.
* Add the current directory `.` into the path `/code` in the image.
* Set the working directory to `/code`.
* Install the Python dependencies.
* Set the default command for the container to `python app.py`
For more information on how to write Dockerfiles, see the [Docker user guide](https://docs.docker.com/engine/userguide/dockerimages/#building-an-image-from-a-dockerfile) and the [Dockerfile reference](http://docs.docker.com/reference/builder/).
2. Build the image.
$ docker build -t web .
This command builds an image named `web` from the contents of the current
directory. The command automatically locates the `Dockerfile`, `app.py`, and
`requirements.txt` files.
## Step 3: Define services
Define a set of services using `docker-compose.yml`:
1. Create a file called docker-compose.yml in your project directory and add
the following:
version: '2'
services:
web:
build: .
ports:
- "5000:5000"
volumes:
- .:/code
depends_on:
- redis
redis:
image: redis
This Compose file defines two services, `web` and `redis`. The web service:
* Builds from the `Dockerfile` in the current directory.
* Forwards the exposed port 5000 on the container to port 5000 on the host machine.
* Mounts the project directory on the host to `/code` inside the container allowing you to modify the code without having to rebuild the image.
* Links the web service to the Redis service.
The `redis` service uses the latest public [Redis](https://registry.hub.docker.com/_/redis/) image pulled from the Docker Hub registry.
## Step 4: Build and run your app with Compose
1. From your project directory, start up your application.
$ docker-compose up
Pulling image redis...
Building web...
Starting composetest_redis_1...
Starting composetest_web_1...
redis_1 | [8] 02 Jan 18:43:35.576 # Server started, Redis version 2.8.3
web_1 | * Running on http://0.0.0.0:5000/
web_1 | * Restarting with stat
Compose pulls a Redis image, builds an image for your code, and start the
services you defined.
2. Enter `http://0.0.0.0:5000/` in a browser to see the application running.
If you're using Docker on Linux natively, then the web app should now be
listening on port 5000 on your Docker daemon host. If http://0.0.0.0:5000
doesn't resolve, you can also try http://localhost:5000.
If you're using Docker Machine on a Mac, use `docker-machine ip MACHINE_VM` to get
the IP address of your Docker host. Then, `open http://MACHINE_VM_IP:5000` in a
browser.
You should see a message in your browser saying:
`Hello World! I have been seen 1 times.`
3. Refresh the page.
The number should increment.
## Step 5: Experiment with some other commands
If you want to run your services in the background, you can pass the `-d` flag
(for "detached" mode) to `docker-compose up` and use `docker-compose ps` to
see what is currently running:
$ docker-compose up -d
Starting composetest_redis_1...
Starting composetest_web_1...
$ docker-compose ps
Name Command State Ports
-------------------------------------------------------------------
composetest_redis_1 /usr/local/bin/run Up
composetest_web_1 /bin/sh -c python app.py Up 5000->5000/tcp
The `docker-compose run` command allows you to run one-off commands for your
services. For example, to see what environment variables are available to the
`web` service:
$ docker-compose run web env
See `docker-compose --help` to see other available commands. You can also install [command completion](completion.md) for the bash and zsh shell, which will also show you available commands.
If you started Compose with `docker-compose up -d`, you'll probably want to stop
your services once you've finished with them:
$ docker-compose stop
At this point, you have seen the basics of how Compose works.
## Where to go next
- Next, try the quick start guide for [Django](django.md),
[Rails](rails.md), or [WordPress](wordpress.md).
- [Explore the full list of Compose commands](./reference/index.md)
- [Compose configuration file reference](compose-file.md)

29
docs/index.md Normal file
View file

@ -0,0 +1,29 @@
<!--[metadata]>
+++
title = "Docker Compose"
description = "Introduction and Overview of Compose"
keywords = ["documentation, docs, docker, compose, orchestration, containers"]
[menu.main]
identifier="workw_compose"
weight=-70
+++
<![end-metadata]-->
# Docker Compose
Compose is a tool for defining and running multi-container Docker applications. To learn more about Compose refer to the following documentation:
- [Compose Overview](overview.md)
- [Install Compose](install.md)
- [Getting Started](gettingstarted.md)
- [Get started with Django](django.md)
- [Get started with Rails](rails.md)
- [Get started with WordPress](wordpress.md)
- [Frequently asked questions](faq.md)
- [Command line reference](./reference/index.md)
- [Compose file reference](compose-file.md)
To see a detailed list of changes for past and current releases of Docker
Compose, please refer to the
[CHANGELOG](https://github.com/docker/compose/blob/master/CHANGELOG.md).

136
docs/install.md Normal file
View file

@ -0,0 +1,136 @@
<!--[metadata]>
+++
title = "Install Compose"
description = "How to install Docker Compose"
keywords = ["compose, orchestration, install, installation, docker, documentation"]
[menu.main]
parent="workw_compose"
weight=-90
+++
<![end-metadata]-->
# Install Docker Compose
You can run Compose on OS X and 64-bit Linux. It is currently not supported on
the Windows operating system. To install Compose, you'll need to install Docker
first.
To install Compose, do the following:
1. Install Docker Engine version 1.7.1 or greater:
* <a href="https://docs.docker.com/engine/installation/mac/" target="_blank">Mac OS X installation</a> (Toolbox installation includes both Engine and Compose)
* <a href="https://docs.docker.com/engine/installation/ubuntulinux/" target="_blank">Ubuntu installation</a>
* <a href="https://docs.docker.com/engine/installation/" target="_blank">other system installations</a>
2. Mac OS X users are done installing. Others should continue to the next step.
3. Go to the <a href="https://github.com/docker/compose/releases" target="_blank">Compose repository release page on GitHub</a>.
4. Follow the instructions from the release page and run the `curl` command,
which the release page specifies, in your terminal.
> Note: If you get a "Permission denied" error, your `/usr/local/bin` directory
probably isn't writable and you'll need to install Compose as the superuser. Run
`sudo -i`, then the two commands below, then `exit`.
The following is an example command illustrating the format:
curl -L https://github.com/docker/compose/releases/download/1.6.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
If you have problems installing with `curl`, see
[Alternative Install Options](#alternative-install-options).
5. Apply executable permissions to the binary:
$ chmod +x /usr/local/bin/docker-compose
6. Optionally, install [command completion](completion.md) for the
`bash` and `zsh` shell.
7. Test the installation.
$ docker-compose --version
docker-compose version: 1.6.1
## Alternative install options
### Install using pip
Compose can be installed from [pypi](https://pypi.python.org/pypi/docker-compose)
using `pip`. If you install using `pip` it is highly recommended that you use a
[virtualenv](https://virtualenv.pypa.io/en/latest/) because many operating systems
have python system packages that conflict with docker-compose dependencies. See
the [virtualenv tutorial](http://docs.python-guide.org/en/latest/dev/virtualenvs/)
to get started.
$ pip install docker-compose
> **Note:** pip version 6.0 or greater is required
### Install as a container
Compose can also be run inside a container, from a small bash script wrapper.
To install compose as a container run:
$ curl -L https://github.com/docker/compose/releases/download/1.6.1/run.sh > /usr/local/bin/docker-compose
$ chmod +x /usr/local/bin/docker-compose
## Master builds
If you're interested in trying out a pre-release build you can download a
binary from https://dl.bintray.com/docker-compose/master/. Pre-release
builds allow you to try out new features before they are released, but may
be less stable.
## Upgrading
If you're upgrading from Compose 1.2 or earlier, you'll need to remove or migrate
your existing containers after upgrading Compose. This is because, as of version
1.3, Compose uses Docker labels to keep track of containers, and so they need to
be recreated with labels added.
If Compose detects containers that were created without labels, it will refuse
to run so that you don't end up with two sets of them. If you want to keep using
your existing containers (for example, because they have data volumes you want
to preserve) you can use compose 1.5.x to migrate them with the following command:
$ docker-compose migrate-to-labels
Alternatively, if you're not worried about keeping them, you can remove them.
Compose will just create new ones.
$ docker rm -f -v myapp_web_1 myapp_db_1 ...
## Uninstallation
To uninstall Docker Compose if you installed using `curl`:
$ rm /usr/local/bin/docker-compose
To uninstall Docker Compose if you installed using `pip`:
$ pip uninstall docker-compose
>**Note**: If you get a "Permission denied" error using either of the above
>methods, you probably do not have the proper permissions to remove
>`docker-compose`. To force the removal, prepend `sudo` to either of the above
>commands and run again.
## Where to go next
- [User guide](index.md)
- [Getting Started](gettingstarted.md)
- [Get started with Django](django.md)
- [Get started with Rails](rails.md)
- [Get started with WordPress](wordpress.md)
- [Command line reference](./reference/index.md)
- [Compose file reference](compose-file.md)

View file

@ -0,0 +1,48 @@
<!--[metadata]>
+++
title = "Link Environment Variables"
description = "Compose CLI reference"
keywords = ["fig, composition, compose, docker, orchestration, cli, reference"]
aliases = ["/compose/env"]
[menu.main]
parent="workw_compose"
weight=89
+++
<![end-metadata]-->
# Link environment variables reference
> **Note:** Environment variables are no longer the recommended method for connecting to linked services. Instead, you should use the link name (by default, the name of the linked service) as the hostname to connect to. See the [docker-compose.yml documentation](compose-file.md#links) for details.
>
> Environment variables will only be populated if you're using the [legacy version 1 Compose file format](compose-file.md#versioning).
Compose uses [Docker links] to expose services' containers to one another. Each linked container injects a set of environment variables, each of which begins with the uppercase name of the container.
To see what environment variables are available to a service, run `docker-compose run SERVICE env`.
<b><i>name</i>\_PORT</b><br>
Full URL, e.g. `DB_PORT=tcp://172.17.0.5:5432`
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i></b><br>
Full URL, e.g. `DB_PORT_5432_TCP=tcp://172.17.0.5:5432`
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i>\_ADDR</b><br>
Container's IP address, e.g. `DB_PORT_5432_TCP_ADDR=172.17.0.5`
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i>\_PORT</b><br>
Exposed port number, e.g. `DB_PORT_5432_TCP_PORT=5432`
<b><i>name</i>\_PORT\_<i>num</i>\_<i>protocol</i>\_PROTO</b><br>
Protocol (tcp or udp), e.g. `DB_PORT_5432_TCP_PROTO=tcp`
<b><i>name</i>\_NAME</b><br>
Fully qualified container name, e.g. `DB_1_NAME=/myapp_web_1/myapp_db_1`
[Docker links]: https://docs.docker.com/engine/userguide/networking/default_network/dockerlinks/
## Related Information
- [User guide](index.md)
- [Installing Compose](install.md)
- [Command line reference](./reference/index.md)
- [Compose file reference](compose-file.md)

152
docs/networking.md Normal file
View file

@ -0,0 +1,152 @@
<!--[metadata]>
+++
title = "Networking in Compose"
description = "How Compose sets up networking between containers"
keywords = ["documentation, docs, docker, compose, orchestration, containers, networking"]
[menu.main]
parent="workw_compose"
weight=21
+++
<![end-metadata]-->
# Networking in Compose
> **Note:** This document only applies if you're using [version 2 of the Compose file format](compose-file.md#versioning). Networking features are not supported for version 1 (legacy) Compose files.
By default Compose sets up a single
[network](/engine/reference/commandline/network_create.md) for your app. Each
container for a service joins the default network and is both *reachable* by
other containers on that network, and *discoverable* by them at a hostname
identical to the container name.
> **Note:** Your app's network is given a name based on the "project name",
> which is based on the name of the directory it lives in. You can override the
> project name with either the [`--project-name`
> flag](reference/overview.md) or the [`COMPOSE_PROJECT_NAME` environment
> variable](reference/envvars.md#compose-project-name).
For example, suppose your app is in a directory called `myapp`, and your `docker-compose.yml` looks like this:
version: '2'
services:
web:
build: .
ports:
- "8000:8000"
db:
image: postgres
When you run `docker-compose up`, the following happens:
1. A network called `myapp_default` is created.
2. A container is created using `web`'s configuration. It joins the network
`myapp_default` under the name `web`.
3. A container is created using `db`'s configuration. It joins the network
`myapp_default` under the name `db`.
Each container can now look up the hostname `web` or `db` and
get back the appropriate container's IP address. For example, `web`'s
application code could connect to the URL `postgres://db:5432` and start
using the Postgres database.
Because `web` explicitly maps a port, it's also accessible from the outside world via port 8000 on your Docker host's network interface.
## Updating containers
If you make a configuration change to a service and run `docker-compose up` to update it, the old container will be removed and the new one will join the network under a different IP address but the same name. Running containers will be able to look up that name and connect to the new address, but the old address will stop working.
If any containers have connections open to the old container, they will be closed. It is a container's responsibility to detect this condition, look up the name again and reconnect.
## Links
Links allow you to define extra aliases by which a service is reachable from another service. They are not required to enable services to communicate - by default, any service can reach any other service at that service's name. In the following example, `db` is reachable from `web` at the hostnames `db` and `database`:
version: '2'
services:
web:
build: .
links:
- "db:database"
db:
image: postgres
See the [links reference](compose-file.md#links) for more information.
## Multi-host networking
When [deploying a Compose application to a Swarm cluster](swarm.md), you can make use of the built-in `overlay` driver to enable multi-host communication between containers with no changes to your Compose file or application code.
Consult the [Getting started with multi-host networking](/engine/userguide/networking/get-started-overlay.md) to see how to set up a Swarm cluster. The cluster will use the `overlay` driver by default, but you can specify it explicitly if you prefer - see below for how to do this.
## Specifying custom networks
Instead of just using the default app network, you can specify your own networks with the top-level `networks` key. This lets you create more complex topologies and specify [custom network drivers](/engine/extend/plugins_network.md) and options. You can also use it to connect services to externally-created networks which aren't managed by Compose.
Each service can specify what networks to connect to with the *service-level* `networks` key, which is a list of names referencing entries under the *top-level* `networks` key.
Here's an example Compose file defining two custom networks. The `proxy` service is isolated from the `db` service, because they do not share a network in common - only `app` can talk to both.
version: '2'
services:
proxy:
build: ./proxy
networks:
- front
app:
build: ./app
networks:
- front
- back
db:
image: postgres
networks:
- back
networks:
front:
# Use a custom driver
driver: custom-driver-1
back:
# Use a custom driver which takes special options
driver: custom-driver-2
driver_opts:
foo: "1"
bar: "2"
For full details of the network configuration options available, see the following references:
- [Top-level `networks` key](compose-file.md#network-configuration-reference)
- [Service-level `networks` key](compose-file.md#networks)
## Configuring the default network
Instead of (or as well as) specifying your own networks, you can also change the settings of the app-wide default network by defining an entry under `networks` named `default`:
version: '2'
services:
web:
build: .
ports:
- "8000:8000"
db:
image: postgres
networks:
default:
# Use a custom driver
driver: custom-driver-1
## Using a pre-existing network
If you want your containers to join a pre-existing network, use the [`external` option](compose-file.md#network-configuration-reference):
networks:
default:
external:
name: my-pre-existing-network
Instead of attemping to create a network called `[projectname]_default`, Compose will look for a network called `my-pre-existing-network` and connect your app's containers to it.

191
docs/overview.md Normal file
View file

@ -0,0 +1,191 @@
<!--[metadata]>
+++
title = "Overview of Docker Compose"
description = "Introduction and Overview of Compose"
keywords = ["documentation, docs, docker, compose, orchestration, containers"]
[menu.main]
parent="workw_compose"
weight=-99
+++
<![end-metadata]-->
# Overview of Docker Compose
Compose is a tool for defining and running multi-container Docker applications.
With Compose, you use a Compose file to configure your application's services.
Then, using a single command, you create and start all the services
from your configuration. To learn more about all the features of Compose
see [the list of features](#features).
Compose is great for development, testing, and staging environments, as well as
CI workflows. You can learn more about each case in
[Common Use Cases](#common-use-cases).
Using Compose is basically a three-step process.
1. Define your app's environment with a `Dockerfile` so it can be
reproduced anywhere.
2. Define the services that make up your app in `docker-compose.yml` so
they can be run together in an isolated environment.
3. Lastly, run `docker-compose up` and Compose will start and run your entire app.
A `docker-compose.yml` looks like this:
version: '2'
services:
web:
build: .
ports:
- "5000:5000"
volumes:
- .:/code
- logvolume01:/var/log
links:
- redis
redis:
image: redis
volumes:
logvolume01: {}
For more information about the Compose file, see the
[Compose file reference](compose-file.md)
Compose has commands for managing the whole lifecycle of your application:
* Start, stop and rebuild services
* View the status of running services
* Stream the log output of running services
* Run a one-off command on a service
## Compose documentation
- [Installing Compose](install.md)
- [Getting Started](gettingstarted.md)
- [Get started with Django](django.md)
- [Get started with Rails](rails.md)
- [Get started with WordPress](wordpress.md)
- [Frequently asked questions](faq.md)
- [Command line reference](./reference/index.md)
- [Compose file reference](compose-file.md)
## Features
The features of Compose that make it effective are:
* [Multiple isolated environments on a single host](#Multiple-isolated-environments-on-a-single-host)
* [Preserve volume data when containers are created](#preserve-volume-data-when-containers-are-created)
* [Only recreate containers that have changed](#only-recreate-containers-that-have-changed)
* [Variables and moving a composition between environments](#variables-and-moving-a-composition-between-environments)
### Multiple isolated environments on a single host
Compose uses a project name to isolate environments from each other. You can use
this project name to:
* on a dev host, to create multiple copies of a single environment (ex: you want
to run a stable copy for each feature branch of a project)
* on a CI server, to keep builds from interfering with each other, you can set
the project name to a unique build number
* on a shared host or dev host, to prevent different projects which may use the
same service names, from interfering with each other
The default project name is the basename of the project directory. You can set
a custom project name by using the
[`-p` command line option](./reference/overview.md) or the
[`COMPOSE_PROJECT_NAME` environment variable](./reference/envvars.md#compose-project-name).
### Preserve volume data when containers are created
Compose preserves all volumes used by your services. When `docker-compose up`
runs, if it finds any containers from previous runs, it copies the volumes from
the old container to the new container. This process ensures that any data
you've created in volumes isn't lost.
### Only recreate containers that have changed
Compose caches the configuration used to create a container. When you
restart a service that has not changed, Compose re-uses the existing
containers. Re-using containers means that you can make changes to your
environment very quickly.
### Variables and moving a composition between environments
Compose supports variables in the Compose file. You can use these variables
to customize your composition for different environments, or different users.
See [Variable substitution](compose-file.md#variable-substitution) for more
details.
You can extend a Compose file using the `extends` field or by creating multiple
Compose files. See [extends](extends.md) for more details.
## Common Use Cases
Compose can be used in many different ways. Some common use cases are outlined
below.
### Development environments
When you're developing software, the ability to run an application in an
isolated environment and interact with it is crucial. The Compose command
line tool can be used to create the environment and interact with it.
The [Compose file](compose-file.md) provides a way to document and configure
all of the application's service dependencies (databases, queues, caches,
web service APIs, etc). Using the Compose command line tool you can create
and start one or more containers for each dependency with a single command
(`docker-compose up`).
Together, these features provide a convenient way for developers to get
started on a project. Compose can reduce a multi-page "developer getting
started guide" to a single machine readable Compose file and a few commands.
### Automated testing environments
An important part of any Continuous Deployment or Continuous Integration process
is the automated test suite. Automated end-to-end testing requires an
environment in which to run tests. Compose provides a convenient way to create
and destroy isolated testing environments for your test suite. By defining the full
environment in a [Compose file](compose-file.md) you can create and destroy these
environments in just a few commands:
$ docker-compose up -d
$ ./run_tests
$ docker-compose down
### Single host deployments
Compose has traditionally been focused on development and testing workflows,
but with each release we're making progress on more production-oriented features.
You can use Compose to deploy to a remote Docker Engine. The Docker Engine may
be a single instance provisioned with
[Docker Machine](https://docs.docker.com/machine/) or an entire
[Docker Swarm](https://docs.docker.com/swarm/) cluster.
For details on using production-oriented features, see
[compose in production](production.md) in this documentation.
## Release Notes
To see a detailed list of changes for past and current releases of Docker
Compose, please refer to the
[CHANGELOG](https://github.com/docker/compose/blob/master/CHANGELOG.md).
## Getting help
Docker Compose is under active development. If you need help, would like to
contribute, or simply want to talk about the project with like-minded
individuals, we have a number of open channels for communication.
* To report bugs or file feature requests: please use the [issue tracker on Github](https://github.com/docker/compose/issues).
* To talk about the project with people in real time: please join the
`#docker-compose` channel on freenode IRC.
* To contribute code or documentation changes: please submit a [pull request on Github](https://github.com/docker/compose/pulls).
For more information and resources, please visit the [Getting Help project page](https://docs.docker.com/opensource/get-help/).

83
docs/production.md Normal file
View file

@ -0,0 +1,83 @@
<!--[metadata]>
+++
title = "Using Compose in Production"
description = "Guide to using Docker Compose in production"
keywords = ["documentation, docs, docker, compose, orchestration, containers, production"]
[menu.main]
parent="workw_compose"
weight=22
+++
<![end-metadata]-->
## Using Compose in production
> Compose is still primarily aimed at development and testing environments.
> Compose may be used for smaller production deployments, but is probably
> not yet suitable for larger deployments.
When deploying to production, you'll almost certainly want to make changes to
your app configuration that are more appropriate to a live environment. These
changes may include:
- Removing any volume bindings for application code, so that code stays inside
the container and can't be changed from outside
- Binding to different ports on the host
- Setting environment variables differently (e.g., to decrease the verbosity of
logging, or to enable email sending)
- Specifying a restart policy (e.g., `restart: always`) to avoid downtime
- Adding extra services (e.g., a log aggregator)
For this reason, you'll probably want to define an additional Compose file, say
`production.yml`, which specifies production-appropriate
configuration. This configuration file only needs to include the changes you'd
like to make from the original Compose file. The additional Compose file
can be applied over the original `docker-compose.yml` to create a new configuration.
Once you've got a second configuration file, tell Compose to use it with the
`-f` option:
$ docker-compose -f docker-compose.yml -f production.yml up -d
See [Using multiple compose files](extends.md#different-environments) for a more
complete example.
### Deploying changes
When you make changes to your app code, you'll need to rebuild your image and
recreate your app's containers. To redeploy a service called
`web`, you would use:
$ docker-compose build web
$ docker-compose up --no-deps -d web
This will first rebuild the image for `web` and then stop, destroy, and recreate
*just* the `web` service. The `--no-deps` flag prevents Compose from also
recreating any services which `web` depends on.
### Running Compose on a single server
You can use Compose to deploy an app to a remote Docker host by setting the
`DOCKER_HOST`, `DOCKER_TLS_VERIFY`, and `DOCKER_CERT_PATH` environment variables
appropriately. For tasks like this,
[Docker Machine](/machine/overview) makes managing local and
remote Docker hosts very easy, and is recommended even if you're not deploying
remotely.
Once you've set up your environment variables, all the normal `docker-compose`
commands will work with no further configuration.
### Running Compose on a Swarm cluster
[Docker Swarm](/swarm/overview), a Docker-native clustering
system, exposes the same API as a single Docker host, which means you can use
Compose against a Swarm instance and run your apps across multiple hosts.
Compose/Swarm integration is still in the experimental stage, but if you'd like
to explore and experiment, check out the [integration guide](swarm.md).
## Compose documentation
- [Installing Compose](install.md)
- [Command line reference](./reference/index.md)
- [Compose file reference](compose-file.md)

144
docs/rails.md Normal file
View file

@ -0,0 +1,144 @@
<!--[metadata]>
+++
title = "Quickstart: Compose and Rails"
description = "Getting started with Docker Compose and Rails"
keywords = ["documentation, docs, docker, compose, orchestration, containers"]
[menu.main]
parent="workw_compose"
weight=5
+++
<![end-metadata]-->
## Quickstart: Compose and Rails
This Quickstart guide will show you how to use Compose to set up and run a Rails/PostgreSQL app. Before starting, you'll need to have [Compose installed](install.md).
### Define the project
Start by setting up the three files you'll need to build the app. First, since
your app is going to run inside a Docker container containing all of its
dependencies, you'll need to define exactly what needs to be included in the
container. This is done using a file called `Dockerfile`. To begin with, the
Dockerfile consists of:
FROM ruby:2.2.0
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev
RUN mkdir /myapp
WORKDIR /myapp
ADD Gemfile /myapp/Gemfile
ADD Gemfile.lock /myapp/Gemfile.lock
RUN bundle install
ADD . /myapp
That'll put your application code inside an image that will build a container with Ruby, Bundler and all your dependencies inside it. For more information on how to write Dockerfiles, see the [Docker user guide](https://docs.docker.com/engine/userguide/dockerimages/#building-an-image-from-a-dockerfile) and the [Dockerfile reference](https://docs.docker.com/engine/reference/builder/).
Next, create a bootstrap `Gemfile` which just loads Rails. It'll be overwritten in a moment by `rails new`.
source 'https://rubygems.org'
gem 'rails', '4.2.0'
You'll need an empty `Gemfile.lock` in order to build our `Dockerfile`.
$ touch Gemfile.lock
Finally, `docker-compose.yml` is where the magic happens. This file describes the services that comprise your app (a database and a web app), how to get each one's Docker image (the database just runs on a pre-made PostgreSQL image, and the web app is built from the current directory), and the configuration needed to link them together and expose the web app's port.
version: '2'
services:
db:
image: postgres
web:
build: .
command: bundle exec rails s -p 3000 -b '0.0.0.0'
volumes:
- .:/myapp
ports:
- "3000:3000"
depends_on:
- db
### Build the project
With those three files in place, you can now generate the Rails skeleton app
using `docker-compose run`:
$ docker-compose run web rails new . --force --database=postgresql --skip-bundle
First, Compose will build the image for the `web` service using the
`Dockerfile`. Then it'll run `rails new` inside a new container, using that
image. Once it's done, you should have generated a fresh app:
$ ls
Dockerfile app docker-compose.yml tmp
Gemfile bin lib vendor
Gemfile.lock config log
README.rdoc config.ru public
Rakefile db test
The files `rails new` created are owned by root. This happens because the
container runs as the `root` user. Change the ownership of the new files.
sudo chown -R $USER:$USER .
Uncomment the line in your new `Gemfile` which loads `therubyracer`, so you've
got a Javascript runtime:
gem 'therubyracer', platforms: :ruby
Now that you've got a new `Gemfile`, you need to build the image again. (This,
and changes to the Dockerfile itself, should be the only times you'll need to
rebuild.)
$ docker-compose build
### Connect the database
The app is now bootable, but you're not quite there yet. By default, Rails
expects a database to be running on `localhost` - so you need to point it at the
`db` container instead. You also need to change the database and username to
align with the defaults set by the `postgres` image.
Replace the contents of `config/database.yml` with the following:
development: &default
adapter: postgresql
encoding: unicode
database: postgres
pool: 5
username: postgres
password:
host: db
test:
<<: *default
database: myapp_test
You can now boot the app with:
$ docker-compose up
If all's well, you should see some PostgreSQL output, and then—after a few
seconds—the familiar refrain:
myapp_web_1 | [2014-01-17 17:16:29] INFO WEBrick 1.3.1
myapp_web_1 | [2014-01-17 17:16:29] INFO ruby 2.2.0 (2014-12-25) [x86_64-linux-gnu]
myapp_web_1 | [2014-01-17 17:16:29] INFO WEBrick::HTTPServer#start: pid=1 port=3000
Finally, you need to create the database. In another terminal, run:
$ docker-compose run web rake db:create
That's it. Your app should now be running on port 3000 on your Docker daemon. If you're using [Docker Machine](https://docs.docker.com/machine/), then `docker-machine ip MACHINE_VM` returns the Docker host IP address.
## More Compose documentation
- [User guide](index.md)
- [Installing Compose](install.md)
- [Getting Started](gettingstarted.md)
- [Get started with Django](django.md)
- [Get started with WordPress](wordpress.md)
- [Command line reference](./reference/index.md)
- [Compose file reference](compose-file.md)

25
docs/reference/build.md Normal file
View file

@ -0,0 +1,25 @@
<!--[metadata]>
+++
title = "build"
description = "build"
keywords = ["fig, composition, compose, docker, orchestration, cli, build"]
[menu.main]
identifier="build.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# build
```
Usage: build [options] [SERVICE...]
Options:
--force-rm Always remove intermediate containers.
--no-cache Do not use cache when building the image.
--pull Always attempt to pull a newer version of the image.
```
Services are built once and then tagged as `project_service`, e.g.,
`composetest_db`. If you change a service's Dockerfile or the contents of its
build directory, run `docker-compose build` to rebuild it.

23
docs/reference/config.md Normal file
View file

@ -0,0 +1,23 @@
<!--[metadata]>
+++
title = "config"
description = "Config validates and view the compose file."
keywords = ["fig, composition, compose, docker, orchestration, cli, config"]
[menu.main]
identifier="config.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# config
```:
Usage: config [options]
Options:
-q, --quiet Only validate the configuration, don't print
anything.
--services Print the service names, one per line.
```
Validate and view the compose file.

25
docs/reference/create.md Normal file
View file

@ -0,0 +1,25 @@
<!--[metadata]>
+++
title = "create"
description = "Create creates containers for a service."
keywords = ["fig, composition, compose, docker, orchestration, cli, create"]
[menu.main]
identifier="create.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# create
```
Usage: create [options] [SERVICE...]
Options:
--force-recreate Recreate containers even if their configuration and
image haven't changed. Incompatible with --no-recreate.
--no-recreate If containers already exist, don't recreate them.
Incompatible with --force-recreate.
--no-build Don't build an image, even if it's missing
```
Creates containers for a service.

26
docs/reference/down.md Normal file
View file

@ -0,0 +1,26 @@
<!--[metadata]>
+++
title = "down"
description = "down"
keywords = ["fig, composition, compose, docker, orchestration, cli, down"]
[menu.main]
identifier="down.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# down
```
Stop containers and remove containers, networks, volumes, and images
created by `up`. Only containers and networks are removed by default.
Usage: down [options]
Options:
--rmi type Remove images, type may be one of: 'all' to remove
all images, or 'local' to remove only images that
don't have an custom name set by the `image` field
-v, --volumes Remove data volumes
```

78
docs/reference/envvars.md Normal file
View file

@ -0,0 +1,78 @@
<!--[metadata]>
+++
title = "CLI Environment Variables"
description = "CLI Environment Variables"
keywords = ["fig, composition, compose, docker, orchestration, cli, reference"]
[menu.main]
parent = "smn_compose_cli"
weight=-1
+++
<![end-metadata]-->
# CLI Environment Variables
Several environment variables are available for you to configure the Docker Compose command-line behaviour.
Variables starting with `DOCKER_` are the same as those used to configure the
Docker command-line client. If you're using `docker-machine`, then the `eval "$(docker-machine env my-docker-vm)"` command should set them to their correct values. (In this example, `my-docker-vm` is the name of a machine you created.)
## COMPOSE\_PROJECT\_NAME
Sets the project name. This value is prepended along with the service name to the container container on start up. For example, if you project name is `myapp` and it includes two services `db` and `web` then compose starts containers named `myapp_db_1` and `myapp_web_1` respectively.
Setting this is optional. If you do not set this, the `COMPOSE_PROJECT_NAME`
defaults to the `basename` of the project directory. See also the `-p`
[command-line option](overview.md).
## COMPOSE\_FILE
Specify the file containing the compose configuration. If not provided,
Compose looks for a file named `docker-compose.yml` in the current directory
and then each parent directory in succession until a file by that name is
found. See also the `-f` [command-line option](overview.md).
## COMPOSE\_API\_VERSION
The Docker API only supports requests from clients which report a specific
version. If you receive a `client and server don't have same version error` using
`docker-compose`, you can workaround this error by setting this environment
variable. Set the version value to match the server version.
Setting this variable is intended as a workaround for situations where you need
to run temporarily with a mismatch between the client and server version. For
example, if you can upgrade the client but need to wait to upgrade the server.
Running with this variable set and a known mismatch does prevent some Docker
features from working properly. The exact features that fail would depend on the
Docker client and server versions. For this reason, running with this variable
set is only intended as a workaround and it is not officially supported.
If you run into problems running with this set, resolve the mismatch through
upgrade and remove this setting to see if your problems resolve before notifying
support.
## DOCKER\_HOST
Sets the URL of the `docker` daemon. As with the Docker client, defaults to `unix:///var/run/docker.sock`.
## DOCKER\_TLS\_VERIFY
When set to anything other than an empty string, enables TLS communication with
the `docker` daemon.
## DOCKER\_CERT\_PATH
Configures the path to the `ca.pem`, `cert.pem`, and `key.pem` files used for TLS verification. Defaults to `~/.docker`.
## COMPOSE\_HTTP\_TIMEOUT
Configures the time (in seconds) a request to the Docker daemon is allowed to hang before Compose considers
it failed. Defaults to 60 seconds.
## Related Information
- [User guide](../index.md)
- [Installing Compose](../install.md)
- [Compose file reference](../compose-file.md)

34
docs/reference/events.md Normal file
View file

@ -0,0 +1,34 @@
<!--[metadata]>
+++
title = "events"
description = "Receive real time events from containers."
keywords = ["fig, composition, compose, docker, orchestration, cli, events"]
[menu.main]
identifier="events.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# events
```
Usage: events [options] [SERVICE...]
Options:
--json Output events as a stream of json objects
```
Stream container events for every container in the project.
With the `--json` flag, a json object will be printed one per line with the
format:
```
{
"service": "web",
"event": "create",
"container": "213cf75fc39a",
"image": "alpine:edge",
"time": "2015-11-20T18:01:03.615550",
}
```

18
docs/reference/help.md Normal file
View file

@ -0,0 +1,18 @@
<!--[metadata]>
+++
title = "help"
description = "help"
keywords = ["fig, composition, compose, docker, orchestration, cli, help"]
[menu.main]
identifier="help.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# help
```
Usage: help COMMAND
```
Displays help and usage instructions for a command.

42
docs/reference/index.md Normal file
View file

@ -0,0 +1,42 @@
<!--[metadata]>
+++
title = "Command-line Reference"
description = "Compose CLI reference"
keywords = ["fig, composition, compose, docker, orchestration, cli, reference"]
[menu.main]
identifier = "smn_compose_cli"
parent = "workw_compose"
weight=80
+++
<![end-metadata]-->
## Compose command-line reference
The following pages describe the usage information for the [docker-compose](overview.md) subcommands. You can also see this information by running `docker-compose [SUBCOMMAND] --help` from the command line.
* [docker-compose](overview.md)
* [build](build.md)
* [config](config.md)
* [create](create.md)
* [down](down.md)
* [events](events.md)
* [help](help.md)
* [kill](kill.md)
* [logs](logs.md)
* [pause](pause.md)
* [port](port.md)
* [ps](ps.md)
* [pull](pull.md)
* [restart](restart.md)
* [rm](rm.md)
* [run](run.md)
* [scale](scale.md)
* [start](start.md)
* [stop](stop.md)
* [unpause](unpause.md)
* [up](up.md)
## Where to go next
* [CLI environment variables](envvars.md)
* [docker-compose Command](overview.md)

24
docs/reference/kill.md Normal file
View file

@ -0,0 +1,24 @@
<!--[metadata]>
+++
title = "kill"
description = "Forces running containers to stop."
keywords = ["fig, composition, compose, docker, orchestration, cli, kill"]
[menu.main]
identifier="kill.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# kill
```
Usage: kill [options] [SERVICE...]
Options:
-s SIGNAL SIGNAL to send to the container. Default signal is SIGKILL.
```
Forces running containers to stop by sending a `SIGKILL` signal. Optionally the
signal can be passed, for example:
$ docker-compose kill -s SIGINT

21
docs/reference/logs.md Normal file
View file

@ -0,0 +1,21 @@
<!--[metadata]>
+++
title = "logs"
description = "Displays log output from services."
keywords = ["fig, composition, compose, docker, orchestration, cli, logs"]
[menu.main]
identifier="logs.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# logs
```
Usage: logs [options] [SERVICE...]
Options:
--no-color Produce monochrome output.
```
Displays log output from services.

117
docs/reference/overview.md Normal file
View file

@ -0,0 +1,117 @@
<!--[metadata]>
+++
title = "Overview of docker-compose CLI"
description = "Overview of docker-compose CLI"
keywords = ["fig, composition, compose, docker, orchestration, cli, docker-compose"]
aliases = ["/compose/reference/docker-compose/"]
[menu.main]
parent = "smn_compose_cli"
weight=-2
+++
<![end-metadata]-->
# Overview of docker-compose CLI
This page provides the usage information for the `docker-compose` Command.
You can also see this information by running `docker-compose --help` from the
command line.
```
Define and run multi-container applications with Docker.
Usage:
docker-compose [-f=<arg>...] [options] [COMMAND] [ARGS...]
docker-compose -h|--help
Options:
-f, --file FILE Specify an alternate compose file (default: docker-compose.yml)
-p, --project-name NAME Specify an alternate project name (default: directory name)
--verbose Show more output
-v, --version Print version and exit
Commands:
build Build or rebuild services
config Validate and view the compose file
create Create services
down Stop and remove containers, networks, images, and volumes
events Receive real time events from containers
help Get help on a command
kill Kill containers
logs View output from containers
pause Pause services
port Print the public port for a port binding
ps List containers
pull Pulls service images
restart Restart services
rm Remove stopped containers
run Run a one-off command
scale Set number of containers for a service
start Start services
stop Stop services
unpause Unpause services
up Create and start containers
version Show the Docker-Compose version information
```
The Docker Compose binary. You use this command to build and manage multiple
services in Docker containers.
Use the `-f` flag to specify the location of a Compose configuration file. You
can supply multiple `-f` configuration files. When you supply multiple files,
Compose combines them into a single configuration. Compose builds the
configuration in the order you supply the files. Subsequent files override and
add to their successors.
For example, consider this command line:
```
$ docker-compose -f docker-compose.yml -f docker-compose.admin.yml run backup_db`
```
The `docker-compose.yml` file might specify a `webapp` service.
```
webapp:
image: examples/web
ports:
- "8000:8000"
volumes:
- "/data"
```
If the `docker-compose.admin.yml` also specifies this same service, any matching
fields will override the previous file. New values, add to the `webapp` service
configuration.
```
webapp:
build: .
environment:
- DEBUG=1
```
Use a `-f` with `-` (dash) as the filename to read the configuration from
stdin. When stdin is used all paths in the configuration are
relative to the current working directory.
The `-f` flag is optional. If you don't provide this flag on the command line,
Compose traverses the working directory and its parent directories looking for a
`docker-compose.yml` and a `docker-compose.override.yml` file. You must
supply at least the `docker-compose.yml` file. If both files are present on the
same directory level, Compose combines the two files into a single configuration.
The configuration in the `docker-compose.override.yml` file is applied over and
in addition to the values in the `docker-compose.yml` file.
See also the `COMPOSE_FILE` [environment variable](envvars.md#compose-file).
Each configuration has a project name. If you supply a `-p` flag, you can
specify a project name. If you don't specify the flag, Compose uses the current
directory name. See also the `COMPOSE_PROJECT_NAME` [environment variable](
envvars.md#compose-project-name)
## Where to go next
* [CLI environment variables](envvars.md)

18
docs/reference/pause.md Normal file
View file

@ -0,0 +1,18 @@
<!--[metadata]>
+++
title = "pause"
description = "Pauses running containers for a service."
keywords = ["fig, composition, compose, docker, orchestration, cli, pause"]
[menu.main]
identifier="pause.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# pause
```
Usage: pause [SERVICE...]
```
Pauses running containers of a service. They can be unpaused with `docker-compose unpause`.

23
docs/reference/port.md Normal file
View file

@ -0,0 +1,23 @@
<!--[metadata]>
+++
title = "port"
description = "Prints the public port for a port binding.s"
keywords = ["fig, composition, compose, docker, orchestration, cli, port"]
[menu.main]
identifier="port.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# port
```
Usage: port [options] SERVICE PRIVATE_PORT
Options:
--protocol=proto tcp or udp [default: tcp]
--index=index index of the container if there are multiple
instances of a service [default: 1]
```
Prints the public port for a port binding.

21
docs/reference/ps.md Normal file
View file

@ -0,0 +1,21 @@
<!--[metadata]>
+++
title = "ps"
description = "Lists containers."
keywords = ["fig, composition, compose, docker, orchestration, cli, ps"]
[menu.main]
identifier="ps.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# ps
```
Usage: ps [options] [SERVICE...]
Options:
-q Only display IDs
```
Lists containers.

21
docs/reference/pull.md Normal file
View file

@ -0,0 +1,21 @@
<!--[metadata]>
+++
title = "pull"
description = "Pulls service images."
keywords = ["fig, composition, compose, docker, orchestration, cli, pull"]
[menu.main]
identifier="pull.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# pull
```
Usage: pull [options] [SERVICE...]
Options:
--ignore-pull-failures Pull what it can and ignores images with pull failures.
```
Pulls service images.

21
docs/reference/restart.md Normal file
View file

@ -0,0 +1,21 @@
<!--[metadata]>
+++
title = "restart"
description = "Restarts Docker Compose services."
keywords = ["fig, composition, compose, docker, orchestration, cli, restart"]
[menu.main]
identifier="restart.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# restart
```
Usage: restart [options] [SERVICE...]
Options:
-t, --timeout TIMEOUT Specify a shutdown timeout in seconds. (default: 10)
```
Restarts services.

27
docs/reference/rm.md Normal file
View file

@ -0,0 +1,27 @@
<!--[metadata]>
+++
title = "rm"
description = "Removes stopped service containers."
keywords = ["fig, composition, compose, docker, orchestration, cli, rm"]
[menu.main]
identifier="rm.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# rm
```
Usage: rm [options] [SERVICE...]
Options:
-f, --force Don't ask to confirm removal
-v Remove volumes associated with containers
```
Removes stopped service containers.
By default, volumes attached to containers will not be removed. You can see all
volumes with `docker volume ls`.
Any data which is not in a volume will be lost.

55
docs/reference/run.md Normal file
View file

@ -0,0 +1,55 @@
<!--[metadata]>
+++
title = "run"
description = "Runs a one-off command on a service."
keywords = ["fig, composition, compose, docker, orchestration, cli, run"]
[menu.main]
identifier="run.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# run
```
Usage: run [options] [-e KEY=VAL...] SERVICE [COMMAND] [ARGS...]
Options:
-d Detached mode: Run container in the background, print
new container name.
--name NAME Assign a name to the container
--entrypoint CMD Override the entrypoint of the image.
-e KEY=VAL Set an environment variable (can be used multiple times)
-u, --user="" Run as specified username or uid
--no-deps Don't start linked services.
--rm Remove container after run. Ignored in detached mode.
-p, --publish=[] Publish a container's port(s) to the host
--service-ports Run command with the service's ports enabled and mapped to the host.
-T Disable pseudo-tty allocation. By default `docker-compose run` allocates a TTY.
```
Runs a one-time command against a service. For example, the following command starts the `web` service and runs `bash` as its command.
$ docker-compose run web bash
Commands you use with `run` start in new containers with the same configuration as defined by the service' configuration. This means the container has the same volumes, links, as defined in the configuration file. There two differences though.
First, the command passed by `run` overrides the command defined in the service configuration. For example, if the `web` service configuration is started with `bash`, then `docker-compose run web python app.py` overrides it with `python app.py`.
The second difference is the `docker-compose run` command does not create any of the ports specified in the service configuration. This prevents the port collisions with already open ports. If you *do want* the service's ports created and mapped to the host, specify the `--service-ports` flag:
$ docker-compose run --service-ports web python manage.py shell
Alternatively manual port mapping can be specified. Same as when running Docker's `run` command - using `--publish` or `-p` options:
$ docker-compose run --publish 8080:80 -p 2022:22 -p 127.0.0.1:2021:21 web python manage.py shell
If you start a service configured with links, the `run` command first checks to see if the linked service is running and starts the service if it is stopped. Once all the linked services are running, the `run` executes the command you passed it. So, for example, you could run:
$ docker-compose run db psql -h db -U docker
This would open up an interactive PostgreSQL shell for the linked `db` container.
If you do not want the `run` command to start linked containers, specify the `--no-deps` flag:
$ docker-compose run --no-deps web python manage.py shell

21
docs/reference/scale.md Normal file
View file

@ -0,0 +1,21 @@
<!--[metadata]>
+++
title = "scale"
description = "Sets the number of containers to run for a service."
keywords = ["fig, composition, compose, docker, orchestration, cli, scale"]
[menu.main]
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# scale
```
Usage: scale [SERVICE=NUM...]
```
Sets the number of containers to run for a service.
Numbers are specified as arguments in the form `service=num`. For example:
$ docker-compose scale web=2 worker=3

18
docs/reference/start.md Normal file
View file

@ -0,0 +1,18 @@
<!--[metadata]>
+++
title = "start"
description = "Starts existing containers for a service."
keywords = ["fig, composition, compose, docker, orchestration, cli, start"]
[menu.main]
identifier="start.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# start
```
Usage: start [SERVICE...]
```
Starts existing containers for a service.

22
docs/reference/stop.md Normal file
View file

@ -0,0 +1,22 @@
<!--[metadata]>
+++
title = "stop"
description = "Stops running containers without removing them. "
keywords = ["fig, composition, compose, docker, orchestration, cli, stop"]
[menu.main]
identifier="stop.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# stop
```
Usage: stop [options] [SERVICE...]
Options:
-t, --timeout TIMEOUT Specify a shutdown timeout in seconds (default: 10).
```
Stops running containers without removing them. They can be started again with
`docker-compose start`.

18
docs/reference/unpause.md Normal file
View file

@ -0,0 +1,18 @@
<!--[metadata]>
+++
title = "unpause"
description = "Unpauses paused containers for a service."
keywords = ["fig, composition, compose, docker, orchestration, cli, unpause"]
[menu.main]
identifier="unpause.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# unpause
```
Usage: unpause [SERVICE...]
```
Unpauses paused containers of a service.

51
docs/reference/up.md Normal file
View file

@ -0,0 +1,51 @@
<!--[metadata]>
+++
title = "up"
description = "Builds, (re)creates, starts, and attaches to containers for a service."
keywords = ["fig, composition, compose, docker, orchestration, cli, up"]
[menu.main]
identifier="up.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# up
```
Usage: up [options] [SERVICE...]
Options:
-d Detached mode: Run containers in the background,
print new container names.
Incompatible with --abort-on-container-exit.
--no-color Produce monochrome output.
--no-deps Don't start linked services.
--force-recreate Recreate containers even if their configuration
and image haven't changed.
Incompatible with --no-recreate.
--no-recreate If containers already exist, don't recreate them.
Incompatible with --force-recreate.
--no-build Don't build an image, even if it's missing
--abort-on-container-exit Stops all containers if any container was stopped.
Incompatible with -d.
-t, --timeout TIMEOUT Use this timeout in seconds for container shutdown
when attached or when containers are already
running. (default: 10)
```
Builds, (re)creates, starts, and attaches to containers for a service.
Unless they are already running, this command also starts any linked services.
The `docker-compose up` command aggregates the output of each container. When
the command exits, all containers are stopped. Running `docker-compose up -d`
starts the containers in the background and leaves them running.
If there are existing containers for a service, and the service's configuration
or image was changed after the container's creation, `docker-compose up` picks
up the changes by stopping and recreating the containers (preserving mounted
volumes). To prevent Compose from picking up changes, use the `--no-recreate`
flag.
If you want to force Compose to stop and recreate all containers, use the
`--force-recreate` flag.

184
docs/swarm.md Normal file
View file

@ -0,0 +1,184 @@
<!--[metadata]>
+++
title = "Using Compose with Swarm"
description = "How to use Compose and Swarm together to deploy apps to multi-host clusters"
keywords = ["documentation, docs, docker, compose, orchestration, containers, swarm"]
[menu.main]
parent="workw_compose"
+++
<![end-metadata]-->
# Using Compose with Swarm
Docker Compose and [Docker Swarm](/swarm/overview) aim to have full integration, meaning
you can point a Compose app at a Swarm cluster and have it all just work as if
you were using a single Docker host.
The actual extent of integration depends on which version of the [Compose file
format](compose-file.md#versioning) you are using:
1. If you're using version 1 along with `links`, your app will work, but Swarm
will schedule all containers on one host, because links between containers
do not work across hosts with the old networking system.
2. If you're using version 2, your app should work with no changes:
- subject to the [limitations](#limitations) described below,
- as long as the Swarm cluster is configured to use the [overlay
driver](/engine/userguide/networking/dockernetworks.md#an-overlay-network),
or a custom driver which supports multi-host networking.
Read the [Getting started with multi-host
networking](/engine/userguide/networking/get-started-overlay.md) to see how to
set up a Swarm cluster with [Docker Machine](/machine/overview) and the overlay driver.
Once you've got it running, deploying your app to it should be as simple as:
$ eval "$(docker-machine env --swarm <name of swarm master machine>)"
$ docker-compose up
## Limitations
### Building images
Swarm can build an image from a Dockerfile just like a single-host Docker
instance can, but the resulting image will only live on a single node and won't
be distributed to other nodes.
If you want to use Compose to scale the service in question to multiple nodes,
you'll have to build it yourself, push it to a registry (e.g. the Docker Hub)
and reference it from `docker-compose.yml`:
$ docker build -t myusername/web .
$ docker push myusername/web
$ cat docker-compose.yml
web:
image: myusername/web
$ docker-compose up -d
$ docker-compose scale web=3
### Multiple dependencies
If a service has multiple dependencies of the type which force co-scheduling
(see [Automatic scheduling](#automatic-scheduling) below), it's possible that
Swarm will schedule the dependencies on different nodes, making the dependent
service impossible to schedule. For example, here `foo` needs to be co-scheduled
with `bar` and `baz`:
version: "2"
services:
foo:
image: foo
volumes_from: ["bar"]
network_mode: "service:baz"
bar:
image: bar
baz:
image: baz
The problem is that Swarm might first schedule `bar` and `baz` on different
nodes (since they're not dependent on one another), making it impossible to
pick an appropriate node for `foo`.
To work around this, use [manual scheduling](#manual-scheduling) to ensure that
all three services end up on the same node:
version: "2"
services:
foo:
image: foo
volumes_from: ["bar"]
network_mode: "service:baz"
environment:
- "constraint:node==node-1"
bar:
image: bar
environment:
- "constraint:node==node-1"
baz:
image: baz
environment:
- "constraint:node==node-1"
### Host ports and recreating containers
If a service maps a port from the host, e.g. `80:8000`, then you may get an
error like this when running `docker-compose up` on it after the first time:
docker: Error response from daemon: unable to find a node that satisfies
container==6ab2dfe36615ae786ef3fc35d641a260e3ea9663d6e69c5b70ce0ca6cb373c02.
The usual cause of this error is that the container has a volume (defined either
in its image or in the Compose file) without an explicit mapping, and so in
order to preserve its data, Compose has directed Swarm to schedule the new
container on the same node as the old container. This results in a port clash.
There are two viable workarounds for this problem:
- Specify a named volume, and use a volume driver which is capable of mounting
the volume into the container regardless of what node it's scheduled on.
Compose does not give Swarm any specific scheduling instructions if a
service uses only named volumes.
version: "2"
services:
web:
build: .
ports:
- "80:8000"
volumes:
- web-logs:/var/log/web
volumes:
web-logs:
driver: custom-volume-driver
- Remove the old container before creating the new one. You will lose any data
in the volume.
$ docker-compose stop web
$ docker-compose rm -f web
$ docker-compose up web
## Scheduling containers
### Automatic scheduling
Some configuration options will result in containers being automatically
scheduled on the same Swarm node to ensure that they work correctly. These are:
- `network_mode: "service:..."` and `network_mode: "container:..."` (and
`net: "container:..."` in the version 1 file format).
- `volumes_from`
- `links`
### Manual scheduling
Swarm offers a rich set of scheduling and affinity hints, enabling you to
control where containers are located. They are specified via container
environment variables, so you can use Compose's `environment` option to set
them.
# Schedule containers on a specific node
environment:
- "constraint:node==node-1"
# Schedule containers on a node that has the 'storage' label set to 'ssd'
environment:
- "constraint:storage==ssd"
# Schedule containers where the 'redis' image is already pulled
environment:
- "affinity:image==redis"
For the full set of available filters and expressions, see the [Swarm
documentation](/swarm/scheduler/filter.md).

104
docs/wordpress.md Normal file
View file

@ -0,0 +1,104 @@
<!--[metadata]>
+++
title = "Quickstart: Compose and WordPress"
description = "Getting started with Compose and WordPress"
keywords = ["documentation, docs, docker, compose, orchestration, containers"]
[menu.main]
parent="workw_compose"
weight=6
+++
<![end-metadata]-->
# Quickstart: Compose and WordPress
You can use Compose to easily run WordPress in an isolated environment built
with Docker containers.
## Define the project
First, [Install Compose](install.md) and then download WordPress into the
current directory:
$ curl https://wordpress.org/latest.tar.gz | tar -xvzf -
This will create a directory called `wordpress`. If you wish, you can rename it
to the name of your project.
Next, inside that directory, create a `Dockerfile`, a file that defines what
environment your app is going to run in. For more information on how to write
Dockerfiles, see the
[Docker user guide](https://docs.docker.com/engine/userguide/dockerimages/#building-an-image-from-a-dockerfile) and the
[Dockerfile reference](https://docs.docker.com/engine/reference/builder/). In
this case, your Dockerfile should be:
FROM orchardup/php5
ADD . /code
This tells Docker how to build an image defining a container that contains PHP
and WordPress.
Next you'll create a `docker-compose.yml` file that will start your web service
and a separate MySQL instance:
version: '2'
services:
web:
build: .
command: php -S 0.0.0.0:8000 -t /code
ports:
- "8000:8000"
depends_on:
- db
volumes:
- .:/code
db:
image: orchardup/mysql
environment:
MYSQL_DATABASE: wordpress
A supporting file is needed to get this working. `wp-config.php` is
the standard WordPress config file with a single change to point the database
configuration at the `db` container:
<?php
define('DB_NAME', 'wordpress');
define('DB_USER', 'root');
define('DB_PASSWORD', '');
define('DB_HOST', "db:3306");
define('DB_CHARSET', 'utf8');
define('DB_COLLATE', '');
define('AUTH_KEY', 'put your unique phrase here');
define('SECURE_AUTH_KEY', 'put your unique phrase here');
define('LOGGED_IN_KEY', 'put your unique phrase here');
define('NONCE_KEY', 'put your unique phrase here');
define('AUTH_SALT', 'put your unique phrase here');
define('SECURE_AUTH_SALT', 'put your unique phrase here');
define('LOGGED_IN_SALT', 'put your unique phrase here');
define('NONCE_SALT', 'put your unique phrase here');
$table_prefix = 'wp_';
define('WPLANG', '');
define('WP_DEBUG', false);
if ( !defined('ABSPATH') )
define('ABSPATH', dirname(__FILE__) . '/');
require_once(ABSPATH . 'wp-settings.php');
### Build the project
With those four files in place, run `docker-compose up` inside your WordPress
directory and it'll pull and build the needed images, and then start the web and
database containers. If you're using [Docker Machine](https://docs.docker.com/machine/), then `docker-machine ip MACHINE_VM` gives you the machine address and you can open `http://MACHINE_VM_IP:8000` in a browser.
## More Compose documentation
- [User guide](index.md)
- [Installing Compose](install.md)
- [Getting Started](gettingstarted.md)
- [Get started with Django](django.md)
- [Get started with Rails](rails.md)
- [Command line reference](./reference/index.md)
- [Compose file reference](compose-file.md)

View file

@ -1,5 +1,183 @@
# Experimental: Compose, Swarm and Multi-Host Networking # Experimental: Compose, Swarm and Multi-Host Networking
Compose now supports multi-host networking as standard. Read more here: The [experimental build of Docker](https://github.com/docker/docker/tree/master/experimental) has an entirely new networking system, which enables secure communication between containers on multiple hosts. In combination with Docker Swarm and Docker Compose, you can now run multi-container apps on multi-host clusters with the same tooling and configuration format you use to develop them locally.
https://docs.docker.com/compose/networking > Note: This functionality is in the experimental stage, and contains some hacks and workarounds which will be removed as it matures.
## Prerequisites
Before you start, youll need to install the experimental build of Docker, and the latest versions of Machine and Compose.
- To install the experimental Docker build on a Linux machine, follow the instructions [here](https://github.com/docker/docker/tree/master/experimental#install-docker-experimental).
- To install the experimental Docker build on a Mac, run these commands:
$ curl -L https://experimental.docker.com/builds/Darwin/x86_64/docker-latest > /usr/local/bin/docker
$ chmod +x /usr/local/bin/docker
- To install Machine, follow the instructions [here](https://docs.docker.com/machine/install-machine/).
- To install Compose, follow the instructions [here](https://docs.docker.com/compose/install/).
Youll also need a [Docker Hub](https://hub.docker.com/account/signup/) account and a [Digital Ocean](https://www.digitalocean.com/) account.
## Set up a swarm with multi-host networking
Set the `DIGITALOCEAN_ACCESS_TOKEN` environment variable to a valid Digital Ocean API token, which you can generate in the [API panel](https://cloud.digitalocean.com/settings/applications).
DIGITALOCEAN_ACCESS_TOKEN=abc12345
Start a consul server:
docker-machine create -d digitalocean --engine-install-url https://experimental.docker.com consul
docker $(docker-machine config consul) run -d -p 8500:8500 -h consul progrium/consul -server -bootstrap
(In a real world setting youd set up a distributed consul, but thats beyond the scope of this guide!)
Create a Swarm token:
SWARM_TOKEN=$(docker run swarm create)
Create a Swarm master:
docker-machine create -d digitalocean --swarm --swarm-master --swarm-discovery=token://$SWARM_TOKEN --engine-install-url="https://experimental.docker.com" --digitalocean-image "ubuntu-14-10-x64" --engine-opt=default-network=overlay:multihost --engine-label=com.docker.network.driver.overlay.bind_interface=eth0 --engine-opt=kv-store=consul:$(docker-machine ip consul):8500 swarm-0
Create a Swarm node:
docker-machine create -d digitalocean --swarm --swarm-discovery=token://$SWARM_TOKEN --engine-install-url="https://experimental.docker.com" --digitalocean-image "ubuntu-14-10-x64" --engine-opt=default-network=overlay:multihost --engine-label=com.docker.network.driver.overlay.bind_interface=eth0 --engine-opt=kv-store=consul:$(docker-machine ip consul):8500 --engine-label com.docker.network.driver.overlay.neighbor_ip=$(docker-machine ip swarm-0) swarm-1
You can create more Swarm nodes if you want - its best to give them sensible names (swarm-2, swarm-3, etc).
Finally, point Docker at your swarm:
eval "$(docker-machine env --swarm swarm-0)"
## Run containers and get them communicating
Now that youve got a swarm up and running, you can create containers on it just like a single Docker instance:
$ docker run busybox echo hello world
hello world
If you run `docker ps -a`, you can see what node that container was started on by looking at its name (here its swarm-3):
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
41f59749737b busybox "echo hello world" 15 seconds ago Exited (0) 13 seconds ago swarm-3/trusting_leakey
As you start more containers, theyll be placed on different nodes across the cluster, thanks to Swarms default “spread” scheduling strategy.
Every container started on this swarm will use the “overlay:multihost” network by default, meaning they can all intercommunicate. Each container gets an IP address on that network, and an `/etc/hosts` file which will be updated on-the-fly with every other containers IP address and name. That means that if you have a running container named foo, other containers can access it at the hostname foo.
Lets verify that multi-host networking is functioning. Start a long-running container:
$ docker run -d --name long-running busybox top
<container id>
If you start a new container and inspect its /etc/hosts file, youll see the long-running container in there:
$ docker run busybox cat /etc/hosts
...
172.21.0.6 long-running
Verify that connectivity works between containers:
$ docker run busybox ping long-running
PING long-running (172.21.0.6): 56 data bytes
64 bytes from 172.21.0.6: seq=0 ttl=64 time=7.975 ms
64 bytes from 172.21.0.6: seq=1 ttl=64 time=1.378 ms
64 bytes from 172.21.0.6: seq=2 ttl=64 time=1.348 ms
^C
--- long-running ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 1.140/2.099/7.975 ms
## Run a Compose application
Heres an example of a simple Python + Redis app using multi-host networking on a swarm.
Create a directory for the app:
$ mkdir composetest
$ cd composetest
Inside this directory, create 2 files.
First, create `app.py` - a simple web app that uses the Flask framework and increments a value in Redis:
from flask import Flask
from redis import Redis
import os
app = Flask(__name__)
redis = Redis(host='composetest_redis_1', port=6379)
@app.route('/')
def hello():
redis.incr('hits')
return 'Hello World! I have been seen %s times.' % redis.get('hits')
if __name__ == "__main__":
app.run(host="0.0.0.0", debug=True)
Note that were connecting to a host called `composetest_redis_1` - this is the name of the Redis container that Compose will start.
Second, create a Dockerfile for the app container:
FROM python:2.7
RUN pip install flask redis
ADD . /code
WORKDIR /code
CMD ["python", "app.py"]
Build the Docker image and push it to the Hub (youll need a Hub account). Replace `<username>` with your Docker Hub username:
$ docker build -t <username>/counter .
$ docker push <username>/counter
Next, create a `docker-compose.yml`, which defines the configuration for the web and redis containers. Once again, replace `<username>` with your Hub username:
web:
image: <username>/counter
ports:
- "80:5000"
redis:
image: redis
Now start the app:
$ docker-compose up -d
Pulling web (username/counter:latest)...
swarm-0: Pulling username/counter:latest... : downloaded
swarm-2: Pulling username/counter:latest... : downloaded
swarm-1: Pulling username/counter:latest... : downloaded
swarm-3: Pulling username/counter:latest... : downloaded
swarm-4: Pulling username/counter:latest... : downloaded
Creating composetest_web_1...
Pulling redis (redis:latest)...
swarm-2: Pulling redis:latest... : downloaded
swarm-1: Pulling redis:latest... : downloaded
swarm-3: Pulling redis:latest... : downloaded
swarm-4: Pulling redis:latest... : downloaded
swarm-0: Pulling redis:latest... : downloaded
Creating composetest_redis_1...
Swarm has created containers for both web and redis, and placed them on different nodes, which you can check with `docker ps`:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
92faad2135c9 redis "/entrypoint.sh redi 43 seconds ago Up 42 seconds swarm-2/composetest_redis_1
adb809e5cdac username/counter "/bin/sh -c 'python 55 seconds ago Up 54 seconds 45.67.8.9:80->5000/tcp swarm-1/composetest_web_1
You can also see that the web container has exposed port 80 on its swarm node. If you curl that IP, youll get a response from the container:
$ curl http://45.67.8.9
Hello World! I have been seen 1 times.
If you hit it repeatedly, the counter will increment, demonstrating that the web and redis container are communicating:
$ curl http://45.67.8.9
Hello World! I have been seen 2 times.
$ curl http://45.67.8.9
Hello World! I have been seen 3 times.
$ curl http://45.67.8.9
Hello World! I have been seen 4 times.

View file

@ -20,30 +20,18 @@ release.
As part of this script you'll be asked to: As part of this script you'll be asked to:
1. Update the version in `compose/__init__.py` and `script/run/run.sh`. 1. Update the version in `docs/install.md` and `compose/__init__.py`.
If the next release will be an RC, append `-rcN`, e.g. `1.4.0-rc1`. If the next release will be an RC, append `rcN`, e.g. `1.4.0rc1`.
2. Write release notes in `CHANGES.md`. 2. Write release notes in `CHANGES.md`.
Almost every feature enhancement should be mentioned, with the most Almost every feature enhancement should be mentioned, with the most visible/exciting ones first. Use descriptive sentences and give context where appropriate.
visible/exciting ones first. Use descriptive sentences and give context
where appropriate.
Bug fixes are worth mentioning if it's likely that they've affected lots Bug fixes are worth mentioning if it's likely that they've affected lots of people, or if they were regressions in the previous version.
of people, or if they were regressions in the previous version.
Improvements to the code are not worth mentioning. Improvements to the code are not worth mentioning.
3. Create a new repository on [bintray](https://bintray.com/docker-compose).
The name has to match the name of the branch (e.g. `bump-1.9.0`) and the
type should be "Generic". Other fields can be left blank.
4. Check that the `vnext-compose` branch on
[the docs repo](https://github.com/docker/docker.github.io/) has
documentation for all the new additions in the upcoming release, and create
a PR there for what needs to be amended.
## When a PR is merged into master that we want in the release ## When a PR is merged into master that we want in the release
@ -67,10 +55,10 @@ Check out the bump branch and run the `build-binaries` script
When prompted build the non-linux binaries and test them. When prompted build the non-linux binaries and test them.
1. Download the osx binary from Bintray. Make sure that the latest Travis 1. Build the Mac binary in a Mountain Lion VM:
build has finished, otherwise you'll be downloading an old binary.
https://dl.bintray.com/docker-compose/$BRANCH_NAME/ script/prepare-osx
script/build-osx
2. Download the windows binary from AppVeyor 2. Download the windows binary from AppVeyor
@ -79,30 +67,28 @@ When prompted build the non-linux binaries and test them.
3. Draft a release from the tag on GitHub (the script will open the window for 3. Draft a release from the tag on GitHub (the script will open the window for
you) you)
The tag will only be present on Github when you run the `push-release` In the "Tag version" dropdown, select the tag you just pushed.
script in step 7, but you can pre-fill it at that point.
4. Paste in installation instructions and release notes. Here's an example - 4. Paste in installation instructions and release notes. Here's an example - change the Compose version and Docker version as appropriate:
change the Compose version and Docker version as appropriate:
If you're a Mac or Windows user, the best way to install Compose and keep it up-to-date is **[Docker for Mac and Windows](https://www.docker.com/products/docker)**. Firstly, note that Compose 1.5.0 requires Docker 1.8.0 or later.
Note that Compose 1.9.0 requires Docker Engine 1.10.0 or later for version 2 of the Compose File format, and Docker Engine 1.9.1 or later for version 1. Docker for Mac and Windows will automatically install the latest version of Docker Engine for you. Secondly, if you're a Mac user, the **[Docker Toolbox](https://www.docker.com/toolbox)** will install Compose 1.5.0 for you, alongside the latest versions of the Docker Engine, Machine and Kitematic.
Alternatively, you can use the usual commands to install or upgrade Compose: Otherwise, you can use the usual commands to install/upgrade. Either download the binary:
``` curl -L https://github.com/docker/compose/releases/download/1.5.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
curl -L https://github.com/docker/compose/releases/download/1.9.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose chmod +x /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
```
See the [install docs](https://docs.docker.com/compose/install/) for more install options and instructions. Or install the PyPi package:
pip install -U docker-compose==1.5.0
Here's what's new: Here's what's new:
...release notes go here... ...release notes go here...
5. Attach the binaries and `script/run/run.sh` 5. Attach the binaries and `script/run.sh`
6. Add "Thanks" with a list of contributors. The contributor list can be generated 6. Add "Thanks" with a list of contributors. The contributor list can be generated
by running `./script/release/contributors`. by running `./script/release/contributors`.
@ -113,8 +99,6 @@ When prompted build the non-linux binaries and test them.
./script/release/push-release ./script/release/push-release
8. Merge the bump PR.
8. Publish the release on GitHub. 8. Publish the release on GitHub.
9. Check that all the binaries download (following the install instructions) and run. 9. Check that all the binaries download (following the install instructions) and run.
@ -123,7 +107,19 @@ When prompted build the non-linux binaries and test them.
## If its a stable release (not an RC) ## If its a stable release (not an RC)
1. Close the releases milestone. 1. Merge the bump PR.
2. Make sure `origin/release` is updated locally:
git fetch origin
3. Update the `docs` branch on the upstream repo:
git push git@github.com:docker/compose.git origin/release:docs
4. Let the docs team know that its been updated so they can publish it.
5. Close the releases milestone.
## If its a minor release (1.x.0), rather than a patch release (1.x.y) ## If its a minor release (1.x.0), rather than a patch release (1.x.y)

View file

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

View file

@ -1,16 +1,11 @@
PyYAML==3.11 PyYAML==3.11
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 docker-py==1.7.1
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
functools32==3.2.3.post2; python_version < '3.2'
ipaddress==1.0.16
jsonschema==2.5.1 jsonschema==2.5.1
pypiwin32==219; sys_platform == 'win32' requests==2.7.0
requests==2.11.1 six==1.7.3
six==1.10.0
texttable==0.8.4 texttable==0.8.4
websocket-client==0.32.0 websocket-client==0.32.0

16
script/build-image Executable file
View file

@ -0,0 +1,16 @@
#!/bin/bash
set -e
if [ -z "$1" ]; then
>&2 echo "First argument must be image tag."
exit 1
fi
TAG=$1
VERSION="$(python setup.py --version)"
./script/write-git-sha
python setup.py sdist
cp dist/docker-compose-$VERSION.tar.gz dist/docker-compose-release.tar.gz
docker build -t docker/compose:$TAG -f Dockerfile.run .

View file

@ -7,7 +7,7 @@ set -ex
TAG="docker-compose" TAG="docker-compose"
docker build -t "$TAG" . | tail -n 200 docker build -t "$TAG" . | tail -n 200
docker run \ docker run \
--rm --entrypoint="script/build/linux-entrypoint" \ --rm --entrypoint="script/build-linux-inner" \
-v $(pwd)/dist:/code/dist \ -v $(pwd)/dist:/code/dist \
-v $(pwd)/.git:/code/.git \ -v $(pwd)/.git:/code/.git \
"$TAG" "$TAG"

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