Compare commits

...

93 commits

Author SHA1 Message Date
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
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
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
41 changed files with 894 additions and 193 deletions

View file

@ -1,6 +1,68 @@
Change log Change log
========== ==========
1.4.2 (2015-09-22)
------------------
Fixes a regression in the 1.4.1 release that would cause `docker-compose up`
without the `-d` option to exit immediately.
1.4.1 (2015-09-10)
------------------
The following bugs have been fixed:
- Some configuration changes (notably changes to `links`, `volumes_from`, and
`net`) were not properly triggering a container recreate as part of
`docker-compose up`.
- `docker-compose up <service>` was showing logs for all services instead of
just the specified services.
- Containers with custom container names were showing up in logs as
`service_number` instead of their custom container name.
- When scaling a service sometimes containers would be recreated even when
the configuration had not changed.
1.4.0 (2015-08-04)
------------------
- By default, `docker-compose up` now only recreates containers for services whose configuration has changed since they were created. This should result in a dramatic speed-up for many applications.
The experimental `--x-smart-recreate` flag which introduced this feature in Compose 1.3.0 has been removed, and a `--force-recreate` flag has been added for when you want to recreate everything.
- Several of Compose's commands - `scale`, `stop`, `kill` and `rm` - now perform actions on multiple containers in parallel, rather than in sequence, which will run much faster on larger applications.
- You can now specify a custom name for a service's container with `container_name`. Because Docker container names must be unique, this means you can't scale the service beyond one container.
- You no longer have to specify a `file` option when using `extends` - it will default to the current file.
- Service names can now contain dots, dashes and underscores.
- Compose can now read YAML configuration from standard input, rather than from a file, by specifying `-` as the filename. This makes it easier to generate configuration dynamically:
$ echo 'redis: {"image": "redis"}' | docker-compose --file - up
- There's a new `docker-compose version` command which prints extended information about Compose's bundled dependencies.
- `docker-compose.yml` now supports `log_opt` as well as `log_driver`, allowing you to pass extra configuration to a service's logging driver.
- `docker-compose.yml` now supports `memswap_limit`, similar to `docker run --memory-swap`.
- When mounting volumes with the `volumes` option, you can now pass in any mode supported by the daemon, not just `:ro` or `:rw`. For example, SELinux users can pass `:z` or `:Z`.
- You can now specify a custom volume driver with the `volume_driver` option in `docker-compose.yml`, much like `docker run --volume-driver`.
- A bug has been fixed where Compose would fail to pull images from private registries serving plain (unsecured) HTTP. The `--allow-insecure-ssl` flag, which was previously used to work around this issue, has been deprecated and now has no effect.
- A bug has been fixed where `docker-compose build` would fail if the build depended on a private Hub image or an image from a private registry.
- A bug has been fixed where Compose would crash if there were containers which the Docker daemon had not finished removing.
- Two bugs have been fixed where Compose would sometimes fail with a "Duplicate bind mount" error, or fail to attach volumes to a container, if there was a volume path specified in `docker-compose.yml` with a trailing slash.
Thanks @mnowster, @dnephin, @ekristen, @funkyfuture, @jeffk and @lukemarsden!
1.3.3 (2015-07-15) 1.3.3 (2015-07-15)
------------------ ------------------

View file

@ -48,16 +48,16 @@ RUN set -ex; \
rm -rf pip-7.0.1; \ rm -rf pip-7.0.1; \
rm pip-7.0.1.tar.gz rm pip-7.0.1.tar.gz
ENV ALL_DOCKER_VERSIONS 1.6.2 1.7.1 ENV ALL_DOCKER_VERSIONS 1.7.1 1.8.0-rc3
RUN set -ex; \ RUN set -ex; \
curl https://get.docker.com/builds/Linux/x86_64/docker-1.6.2 -o /usr/local/bin/docker-1.6.2; \
chmod +x /usr/local/bin/docker-1.6.2; \
curl https://get.docker.com/builds/Linux/x86_64/docker-1.7.1 -o /usr/local/bin/docker-1.7.1; \ curl https://get.docker.com/builds/Linux/x86_64/docker-1.7.1 -o /usr/local/bin/docker-1.7.1; \
chmod +x /usr/local/bin/docker-1.7.1 chmod +x /usr/local/bin/docker-1.7.1; \
curl https://test.docker.com/builds/Linux/x86_64/docker-1.8.0-rc3 -o /usr/local/bin/docker-1.8.0-rc3; \
chmod +x /usr/local/bin/docker-1.8.0-rc3
# Set the default Docker to be run # Set the default Docker to be run
RUN ln -s /usr/local/bin/docker-1.6.2 /usr/local/bin/docker RUN ln -s /usr/local/bin/docker-1.7.1 /usr/local/bin/docker
RUN useradd -d /home/user -m -s /bin/bash user RUN useradd -d /home/user -m -s /bin/bash user
WORKDIR /code/ WORKDIR /code/

View file

@ -1,3 +1,3 @@
from __future__ import unicode_literals from __future__ import unicode_literals
__version__ = '1.4.0dev' __version__ = '1.4.2'

View file

@ -14,6 +14,8 @@ def docker_client():
cert_path = os.path.join(os.environ.get('HOME', ''), '.docker') cert_path = os.path.join(os.environ.get('HOME', ''), '.docker')
base_url = os.environ.get('DOCKER_HOST') base_url = os.environ.get('DOCKER_HOST')
api_version = os.environ.get('COMPOSE_API_VERSION', '1.19')
tls_config = None tls_config = None
if os.environ.get('DOCKER_TLS_VERIFY', '') != '': if os.environ.get('DOCKER_TLS_VERIFY', '') != '':
@ -32,4 +34,4 @@ def docker_client():
) )
timeout = int(os.environ.get('DOCKER_CLIENT_TIMEOUT', 60)) timeout = int(os.environ.get('DOCKER_CLIENT_TIMEOUT', 60))
return Client(base_url=base_url, tls=tls_config, version='1.18', timeout=timeout) return Client(base_url=base_url, tls=tls_config, version=api_version, timeout=timeout)

View file

@ -304,7 +304,7 @@ class TopLevelCommand(Command):
log.warn(INSECURE_SSL_WARNING) log.warn(INSECURE_SSL_WARNING)
if not options['--no-deps']: if not options['--no-deps']:
deps = service.get_linked_names() deps = service.get_linked_service_names()
if len(deps) > 0: if len(deps) > 0:
project.up( project.up(
@ -496,19 +496,8 @@ class TopLevelCommand(Command):
) )
if not detached: if not detached:
print("Attaching to", list_containers(to_attach)) log_printer = build_log_printer(to_attach, service_names, monochrome)
log_printer = LogPrinter(to_attach, attach_params={"logs": True}, monochrome=monochrome) attach_to_logs(project, log_printer, service_names, timeout)
try:
log_printer.run()
finally:
def handler(signal, frame):
project.kill(service_names=service_names)
sys.exit(0)
signal.signal(signal.SIGINT, handler)
print("Gracefully stopping... (press Ctrl+C again to force)")
project.stop(service_names=service_names, timeout=timeout)
def migrate_to_labels(self, project, _options): def migrate_to_labels(self, project, _options):
""" """
@ -551,5 +540,29 @@ class TopLevelCommand(Command):
print(get_version_info('full')) print(get_version_info('full'))
def build_log_printer(containers, service_names, monochrome):
if service_names:
containers = [c for c in containers if c.service in service_names]
return LogPrinter(
containers,
attach_params={"logs": True},
monochrome=monochrome)
def attach_to_logs(project, log_printer, service_names, timeout):
print("Attaching to", list_containers(log_printer.containers))
try:
log_printer.run()
finally:
def handler(signal, frame):
project.kill(service_names=service_names)
sys.exit(0)
signal.signal(signal.SIGINT, handler)
print("Gracefully stopping... (press Ctrl+C again to force)")
project.stop(service_names=service_names, timeout=timeout)
def list_containers(containers): def list_containers(containers):
return ", ".join(c.name for c in containers) return ", ".join(c.name for c in containers)

View file

@ -12,9 +12,9 @@ from compose.cli.utils import find_candidates_in_parent_dirs
DOCKER_CONFIG_KEYS = [ DOCKER_CONFIG_KEYS = [
'cap_add', 'cap_add',
'cap_drop', 'cap_drop',
'command',
'cpu_shares', 'cpu_shares',
'cpuset', 'cpuset',
'command',
'detach', 'detach',
'devices', 'devices',
'dns', 'dns',
@ -28,12 +28,12 @@ DOCKER_CONFIG_KEYS = [
'image', 'image',
'labels', 'labels',
'links', 'links',
'log_driver',
'log_opt',
'mac_address', 'mac_address',
'mem_limit', 'mem_limit',
'memswap_limit', 'memswap_limit',
'net', 'net',
'log_driver',
'log_opt',
'pid', 'pid',
'ports', 'ports',
'privileged', 'privileged',
@ -43,6 +43,7 @@ DOCKER_CONFIG_KEYS = [
'stdin_open', 'stdin_open',
'tty', 'tty',
'user', 'user',
'volume_driver',
'volumes', 'volumes',
'volumes_from', 'volumes_from',
'working_dir', 'working_dir',
@ -82,6 +83,13 @@ SUPPORTED_FILENAMES = [
] ]
PATH_START_CHARS = [
'/',
'.',
'~',
]
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -251,8 +259,8 @@ def process_container_options(service_dict, working_dir=None):
if 'memswap_limit' in service_dict and 'mem_limit' not in service_dict: if 'memswap_limit' in service_dict and 'mem_limit' not in service_dict:
raise ConfigurationError("Invalid 'memswap_limit' configuration for %s service: when defining 'memswap_limit' you must set 'mem_limit' as well" % service_dict['name']) raise ConfigurationError("Invalid 'memswap_limit' configuration for %s service: when defining 'memswap_limit' you must set 'mem_limit' as well" % service_dict['name'])
if 'volumes' in service_dict: if 'volumes' in service_dict and service_dict.get('volume_driver') is None:
service_dict['volumes'] = resolve_volume_paths(service_dict['volumes'], working_dir=working_dir) service_dict['volumes'] = resolve_volume_paths(service_dict, working_dir=working_dir)
if 'build' in service_dict: if 'build' in service_dict:
service_dict['build'] = resolve_build_path(service_dict['build'], working_dir=working_dir) service_dict['build'] = resolve_build_path(service_dict['build'], working_dir=working_dir)
@ -374,7 +382,7 @@ def parse_environment(environment):
return dict(split_env(e) for e in environment) return dict(split_env(e) for e in environment)
if isinstance(environment, dict): if isinstance(environment, dict):
return environment return dict(environment)
raise ConfigurationError( raise ConfigurationError(
"environment \"%s\" must be a list or mapping," % "environment \"%s\" must be a list or mapping," %
@ -413,18 +421,33 @@ def env_vars_from_file(filename):
return env return env
def resolve_volume_paths(volumes, working_dir=None): def resolve_volume_paths(service_dict, working_dir=None):
if working_dir is None: if working_dir is None:
raise Exception("No working_dir passed to resolve_volume_paths()") raise Exception("No working_dir passed to resolve_volume_paths()")
return [resolve_volume_path(v, working_dir) for v in volumes] return [
resolve_volume_path(v, working_dir, service_dict['name'])
for v in service_dict['volumes']
]
def resolve_volume_path(volume, working_dir): def resolve_volume_path(volume, working_dir, service_name):
container_path, host_path = split_path_mapping(volume) container_path, host_path = split_path_mapping(volume)
container_path = os.path.expanduser(os.path.expandvars(container_path)) container_path = os.path.expanduser(os.path.expandvars(container_path))
if host_path is not None: if host_path is not None:
host_path = os.path.expanduser(os.path.expandvars(host_path)) host_path = os.path.expanduser(os.path.expandvars(host_path))
if not any(host_path.startswith(c) for c in PATH_START_CHARS):
log.warn(
'Warning: the mapping "{0}:{1}" in the volumes config for '
'service "{2}" is ambiguous. In a future version of Docker, '
'it will designate a "named" volume '
'(see https://github.com/docker/docker/pull/14242). '
'To prevent unexpected behaviour, change it to "./{0}:{1}"'
.format(host_path, container_path, service_name)
)
return "%s:%s" % (expand_path(working_dir, host_path), container_path) return "%s:%s" % (expand_path(working_dir, host_path), container_path)
else: else:
return container_path return container_path

View file

@ -22,10 +22,14 @@ class Container(object):
""" """
Construct a container object from the output of GET /containers/json. Construct a container object from the output of GET /containers/json.
""" """
name = get_container_name(dictionary)
if name is None:
return None
new_dictionary = { new_dictionary = {
'Id': dictionary['Id'], 'Id': dictionary['Id'],
'Image': dictionary['Image'], 'Image': dictionary['Image'],
'Name': '/' + get_container_name(dictionary), 'Name': '/' + name,
} }
return cls(client, new_dictionary, **kwargs) return cls(client, new_dictionary, **kwargs)
@ -58,9 +62,13 @@ class Container(object):
def name(self): def name(self):
return self.dictionary['Name'][1:] return self.dictionary['Name'][1:]
@property
def service(self):
return self.labels.get(LABEL_SERVICE)
@property @property
def name_without_project(self): def name_without_project(self):
return '{0}_{1}'.format(self.labels.get(LABEL_SERVICE), self.number) return '{0}_{1}'.format(self.service, self.number)
@property @property
def number(self): def number(self):

View file

@ -9,7 +9,10 @@ from .config import get_service_name_from_net, ConfigurationError
from .const import DEFAULT_TIMEOUT, LABEL_PROJECT, LABEL_SERVICE, LABEL_ONE_OFF from .const import DEFAULT_TIMEOUT, LABEL_PROJECT, LABEL_SERVICE, LABEL_ONE_OFF
from .container import Container from .container import Container
from .legacy import check_for_legacy_containers from .legacy import check_for_legacy_containers
from .service import ContainerNet
from .service import Net
from .service import Service from .service import Service
from .service import ServiceNet
from .utils import parallel_execute from .utils import parallel_execute
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -81,8 +84,14 @@ class Project(object):
volumes_from = project.get_volumes_from(service_dict) volumes_from = project.get_volumes_from(service_dict)
net = project.get_net(service_dict) net = project.get_net(service_dict)
project.services.append(Service(client=client, project=name, links=links, net=net, project.services.append(
volumes_from=volumes_from, **service_dict)) Service(
client=client,
project=name,
links=links,
net=net,
volumes_from=volumes_from,
**service_dict))
return project return project
@property @property
@ -172,26 +181,26 @@ class Project(object):
return volumes_from return volumes_from
def get_net(self, service_dict): def get_net(self, service_dict):
if 'net' in service_dict: net = service_dict.pop('net', None)
net_name = get_service_name_from_net(service_dict.get('net')) if not net:
return Net(None)
if net_name: net_name = get_service_name_from_net(net)
try: if not net_name:
net = self.get_service(net_name) return Net(net)
except NoSuchService:
try:
net = Container.from_id(self.client, net_name)
except APIError:
raise ConfigurationError('Service "%s" is trying to use the network of "%s", which is not the name of a service or container.' % (service_dict['name'], net_name))
else:
net = service_dict['net']
del service_dict['net'] try:
return ServiceNet(self.get_service(net_name))
else: except NoSuchService:
net = None pass
try:
return net return ContainerNet(Container.from_id(self.client, net_name))
except APIError:
raise ConfigurationError(
'Service "%s" is trying to use the network of "%s", '
'which is not the name of a service or container.' % (
service_dict['name'],
net_name))
def start(self, service_names=None, **options): def start(self, service_names=None, **options):
for service in self.get_services(service_names): for service in self.get_services(service_names):
@ -310,11 +319,11 @@ class Project(object):
else: else:
service_names = self.service_names service_names = self.service_names
containers = [ containers = filter(None, [
Container.from_ps(self.client, container) Container.from_ps(self.client, container)
for container in self.client.containers( for container in self.client.containers(
all=stopped, all=stopped,
filters={'label': self.labels(one_off=one_off)})] 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

View file

@ -3,6 +3,7 @@ from __future__ import absolute_import
from collections import namedtuple from collections import namedtuple
import logging import logging
import re import re
import os
import sys import sys
from operator import attrgetter from operator import attrgetter
@ -41,6 +42,8 @@ DOCKER_START_KEYS = [
'net', 'net',
'log_driver', 'log_driver',
'log_opt', 'log_opt',
'mem_limit',
'memswap_limit',
'pid', 'pid',
'privileged', 'privileged',
'restart', 'restart',
@ -80,7 +83,16 @@ ConvergencePlan = namedtuple('ConvergencePlan', 'action containers')
class Service(object): class Service(object):
def __init__(self, name, client=None, project='default', links=None, external_links=None, volumes_from=None, net=None, **options): def __init__(
self,
name,
client=None,
project='default',
links=None,
volumes_from=None,
net=None,
**options
):
if not re.match('^%s+$' % VALID_NAME_CHARS, name): if not re.match('^%s+$' % VALID_NAME_CHARS, name):
raise ConfigError('Invalid service name "%s" - only %s are allowed' % (name, VALID_NAME_CHARS)) raise ConfigError('Invalid service name "%s" - only %s are allowed' % (name, VALID_NAME_CHARS))
if not re.match('^%s+$' % VALID_NAME_CHARS, project): if not re.match('^%s+$' % VALID_NAME_CHARS, project):
@ -94,17 +106,16 @@ class Service(object):
self.client = client self.client = client
self.project = project self.project = project
self.links = links or [] self.links = links or []
self.external_links = external_links or []
self.volumes_from = volumes_from or [] self.volumes_from = volumes_from or []
self.net = net or None self.net = net or Net(None)
self.options = options self.options = options
def containers(self, stopped=False, one_off=False): def containers(self, stopped=False, one_off=False):
containers = [ containers = filter(None, [
Container.from_ps(self.client, container) Container.from_ps(self.client, container)
for container in self.client.containers( for container in self.client.containers(
all=stopped, all=stopped,
filters={'label': self.labels(one_off=one_off)})] filters={'label': self.labels(one_off=one_off)})])
if not containers: if not containers:
check_for_legacy_containers( check_for_legacy_containers(
@ -466,26 +477,26 @@ class Service(object):
return { return {
'options': self.options, 'options': self.options,
'image_id': self.image()['Id'], 'image_id': self.image()['Id'],
'links': self.get_link_names(),
'net': self.net.id,
'volumes_from': self.get_volumes_from_names(),
} }
def get_dependency_names(self): def get_dependency_names(self):
net_name = self.get_net_name() net_name = self.net.service_name
return (self.get_linked_names() + return (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 []))
def get_linked_names(self): def get_linked_service_names(self):
return [s.name for (s, _) in self.links] return [service.name for (service, _) in self.links]
def get_link_names(self):
return [(service.name, alias) for service, alias in self.links]
def get_volumes_from_names(self): def get_volumes_from_names(self):
return [s.name for s in self.volumes_from if isinstance(s, Service)] return [s.name for s in self.volumes_from if isinstance(s, Service)]
def get_net_name(self):
if isinstance(self.net, Service):
return self.net.name
else:
return
def get_container_name(self, number, one_off=False): def get_container_name(self, number, one_off=False):
# TODO: Implement issue #652 here # TODO: Implement issue #652 here
return build_container_name(self.project, self.name, number, one_off) return build_container_name(self.project, self.name, number, one_off)
@ -493,12 +504,13 @@ class Service(object):
# TODO: this would benefit from github.com/docker/docker/pull/11943 # TODO: this would benefit from github.com/docker/docker/pull/11943
# 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):
numbers = [ containers = filter(None, [
Container.from_ps(self.client, container).number Container.from_ps(self.client, container)
for container in self.client.containers( for container in self.client.containers(
all=True, all=True,
filters={'label': self.labels(one_off=one_off)}) filters={'label': self.labels(one_off=one_off)})
] ])
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_links(self, link_to_self): def _get_links(self, link_to_self):
@ -513,7 +525,7 @@ class Service(object):
links.append((container.name, self.name)) links.append((container.name, self.name))
links.append((container.name, container.name)) links.append((container.name, container.name))
links.append((container.name, container.name_without_project)) links.append((container.name, container.name_without_project))
for external_link in self.external_links: for external_link in self.options.get('external_links') or []:
if ':' not in external_link: if ':' not in external_link:
link_name = external_link link_name = external_link
else: else:
@ -536,32 +548,12 @@ class Service(object):
return volumes_from return volumes_from
def _get_net(self):
if not self.net:
return None
if isinstance(self.net, Service):
containers = self.net.containers()
if len(containers) > 0:
net = 'container:' + containers[0].id
else:
log.warning("Warning: Service %s is trying to use reuse the network stack "
"of another service that is not running." % (self.net.name))
net = None
elif isinstance(self.net, Container):
net = 'container:' + self.net.id
else:
net = self.net
return net
def _get_container_create_options( def _get_container_create_options(
self, self,
override_options, override_options,
number, number,
one_off=False, one_off=False,
previous_container=None): previous_container=None):
add_config_hash = (not one_off and not override_options) add_config_hash = (not one_off and not override_options)
container_options = dict( container_options = dict(
@ -574,13 +566,6 @@ class Service(object):
else: else:
container_options['name'] = self.get_container_name(number, one_off) container_options['name'] = self.get_container_name(number, one_off)
if add_config_hash:
config_hash = self.config_hash()
if 'labels' not in container_options:
container_options['labels'] = {}
container_options['labels'][LABEL_CONFIG_HASH] = config_hash
log.debug("Added config hash: %s" % config_hash)
if 'detach' not in container_options: if 'detach' not in container_options:
container_options['detach'] = True container_options['detach'] = True
@ -628,7 +613,8 @@ class Service(object):
container_options['labels'] = build_container_labels( container_options['labels'] = build_container_labels(
container_options.get('labels', {}), container_options.get('labels', {}),
self.labels(one_off=one_off), self.labels(one_off=one_off),
number) number,
self.config_hash() if add_config_hash else None)
# Delete options which are only used when starting # Delete options which are only used when starting
for key in DOCKER_START_KEYS: for key in DOCKER_START_KEYS:
@ -675,13 +661,15 @@ class Service(object):
binds=options.get('binds'), binds=options.get('binds'),
volumes_from=self._get_volumes_from(), volumes_from=self._get_volumes_from(),
privileged=privileged, privileged=privileged,
network_mode=self._get_net(), network_mode=self.net.mode,
devices=devices, devices=devices,
dns=dns, dns=dns,
dns_search=dns_search, dns_search=dns_search,
restart_policy=restart, restart_policy=restart,
cap_add=cap_add, cap_add=cap_add,
cap_drop=cap_drop, cap_drop=cap_drop,
mem_limit=options.get('mem_limit'),
memswap_limit=options.get('memswap_limit'),
log_config=log_config, log_config=log_config,
extra_hosts=extra_hosts, extra_hosts=extra_hosts,
read_only=read_only, read_only=read_only,
@ -768,6 +756,61 @@ class Service(object):
stream_output(output, sys.stdout) stream_output(output, sys.stdout)
class Net(object):
"""A `standard` network mode (ex: host, bridge)"""
service_name = None
def __init__(self, net):
self.net = net
@property
def id(self):
return self.net
mode = id
class ContainerNet(object):
"""A network mode that uses a container's network stack."""
service_name = None
def __init__(self, container):
self.container = container
@property
def id(self):
return self.container.id
@property
def mode(self):
return 'container:' + self.container.id
class ServiceNet(object):
"""A network mode that uses a service's network stack."""
def __init__(self, service):
self.service = service
@property
def id(self):
return self.service.name
service_name = id
@property
def mode(self):
containers = self.service.containers()
if containers:
return 'container:' + containers[0].id
log.warn("Warning: Service %s is trying to use reuse the network stack "
"of another service that is not running." % (self.id))
return None
# Names # Names
@ -848,12 +891,15 @@ def parse_volume_spec(volume_config):
"external:internal[:mode]" % volume_config) "external:internal[:mode]" % volume_config)
if len(parts) == 1: if len(parts) == 1:
return VolumeSpec(None, parts[0], 'rw') external = None
internal = os.path.normpath(parts[0])
else:
external = os.path.normpath(parts[0])
internal = os.path.normpath(parts[1])
if len(parts) == 2: mode = parts[2] if len(parts) == 3 else 'rw'
parts.append('rw')
return VolumeSpec(*parts) return VolumeSpec(external, internal, mode)
# Ports # Ports
@ -890,11 +936,16 @@ def split_port(port):
# Labels # Labels
def build_container_labels(label_options, service_labels, number, one_off=False): def build_container_labels(label_options, service_labels, number, config_hash):
labels = label_options or {} labels = dict(label_options or {})
labels.update(label.split('=', 1) for label in service_labels) labels.update(label.split('=', 1) for label in service_labels)
labels[LABEL_CONTAINER_NUMBER] = str(number) labels[LABEL_CONTAINER_NUMBER] = str(number)
labels[LABEL_VERSION] = __version__ labels[LABEL_VERSION] = __version__
if config_hash:
log.debug("Added config hash: %s" % config_hash)
labels[LABEL_CONFIG_HASH] = config_hash
return labels return labels

View file

@ -32,6 +32,10 @@ def parallel_execute(objects, obj_callable, msg_index, msg):
except APIError as e: except APIError as e:
errors[msg_index] = e.explanation errors[msg_index] = e.explanation
result = "error" result = "error"
except Exception as e:
errors[msg_index] = e
result = 'unexpected_exception'
q.put((msg_index, result)) q.put((msg_index, result))
for an_object in objects: for an_object in objects:
@ -48,6 +52,9 @@ def parallel_execute(objects, obj_callable, msg_index, msg):
while done < total_to_execute: while done < total_to_execute:
try: try:
msg_index, result = q.get(timeout=1) msg_index, result = q.get(timeout=1)
if result == 'unexpected_exception':
raise errors[msg_index]
if result == 'error': if result == 'error':
write_out_msg(stream, lines, msg_index, msg, status='error') write_out_msg(stream, lines, msg_index, msg, status='error')
else: else:

BIN
docker Executable file

Binary file not shown.

View file

@ -6,6 +6,14 @@ COPY . /src
COPY . /docs/content/compose/ COPY . /docs/content/compose/
RUN svn checkout https://github.com/docker/docker/trunk/docs /docs/content/docker
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/tutorials/trunk/docs /docs/content/tutorials
RUN svn checkout https://github.com/docker/opensource/trunk/docs /docs/content
# Sed to process GitHub Markdown # Sed to process GitHub Markdown
# 1-2 Remove comment code from metadata block # 1-2 Remove comment code from metadata block
# 3 Change ](/word to ](/project/ in links # 3 Change ](/word to ](/project/ in links
@ -15,10 +23,4 @@ COPY . /docs/content/compose/
# 7 Change ](../../ to ](/project/ --> not implemented # 7 Change ](../../ to ](/project/ --> not implemented
# #
# #
RUN find /docs/content/compose -type f -name "*.md" -exec sed -i.old \ RUN /src/pre-process.sh /docs
-e '/^<!.*metadata]>/g' \
-e '/^<!.*end-metadata.*>/g' \
-e 's/\(\]\)\([(]\)\(\/\)/\1\2\/compose\//g' \
-e 's/\(\][(]\)\([A-z].*\)\(\.md\)/\1\/compose\/\2/g' \
-e 's/\([(]\)\(.*\)\(\.md\)/\1\2/g' \
-e 's/\(\][(]\)\(\.\.\/\)/\1\/compose\//g' {} \;

View file

@ -12,48 +12,67 @@ weight=4
# Install Docker Compose # Install Docker Compose
To install Compose, you'll need to install Docker first. You'll then install You can run Compose on OS X and 64-bit Linux. It is currently not supported on
Compose with a `curl` command. the Windows operating system. To install Compose, you'll need to install Docker
first.
## Install Docker Depending on how your system is configured, you may require `sudo` access to
install Compose. If your system requires `sudo`, you will receive "Permission
denied" errors when installing Compose. If this is the case for you, preface the
install commands with `sudo` to install.
First, install Docker version 1.6 or greater: To install Compose, do the following:
- [Instructions for Mac OS X](http://docs.docker.com/installation/mac/) 1. Install Docker Engine version 1.7.1 or greater:
- [Instructions for Ubuntu](http://docs.docker.com/installation/ubuntulinux/)
- [Instructions for other systems](http://docs.docker.com/installation/)
## Install Compose * <a href="https://docs.docker.com/installation/mac/" target="_blank">Mac OS X installation</a> (installs both Engine and Compose)
* <a href="https://docs.docker.com/installation/ubuntulinux/" target="_blank">Ubuntu installation</a>
* <a href="https://docs.docker.com/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">repository release page</a>.
To install Compose, run the following commands: 4. Enter the `curl` command in your termial.
curl -L https://github.com/docker/compose/releases/download/1.3.3/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose The command has the following format:
chmod +x /usr/local/bin/docker-compose
> 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 above, then `exit`. curl -L https://github.com/docker/compose/releases/download/VERSION_NUM/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
If you have problems installing with `curl`, you can use `pip` instead: `pip install -U docker-compose`
4. Apply executable permissions to the binary:
Optionally, you can also install [command completion](completion.md) for the $ chmod +x /usr/local/bin/docker-compose
bash and zsh shell.
Compose is available for OS X and 64-bit Linux. If you're on another platform, 5. Optionally, install [command completion](completion.md) for the
Compose can also be installed as a Python package: `bash` and `zsh` shell.
$ sudo pip install -U docker-compose 6. Test the installation.
No further steps are required; Compose should now be successfully installed. $ docker-compose --version
You can test the installation by running `docker-compose --version`. docker-compose version: 1.4.2
### Upgrading ## Upgrading
If you're coming 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 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 migrate them with the following command: 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 migrate them with the following command:
docker-compose migrate-to-labels $ docker-compose migrate-to-labels
Alternatively, if you're not worried about keeping them, you can remove them - Compose will just create new ones. Alternatively, if you're not worried about keeping them, you can remove them &endash;
Compose will just create new ones.
docker rm -f myapp_web_1 myapp_db_1 ... $ docker rm -f -v myapp_web_1 myapp_db_1 ...
## Uninstallation ## Uninstallation
@ -67,10 +86,13 @@ To uninstall Docker Compose if you installed using `pip`:
$ pip uninstall docker-compose $ 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. >**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.
## Compose documentation ## Where to go next
- [User guide](/) - [User guide](/)
- [Get started with Django](django.md) - [Get started with Django](django.md)

61
docs/pre-process.sh Executable file
View file

@ -0,0 +1,61 @@
#!/bin/bash -e
# Populate an array with just docker dirs and one with content dirs
docker_dir=(`ls -d /docs/content/docker/*`)
content_dir=(`ls -d /docs/content/*`)
# Loop content not of docker/
#
# Sed to process GitHub Markdown
# 1-2 Remove comment code from metadata block
# 3 Remove .md extension from link text
# 4 Change ](/ to ](/project/ in links
# 5 Change ](word) to ](/project/word)
# 6 Change ](../../ to ](/project/
# 7 Change ](../ to ](/project/word)
#
for i in "${content_dir[@]}"
do
:
case $i in
"/docs/content/windows")
;;
"/docs/content/mac")
;;
"/docs/content/linux")
;;
"/docs/content/docker")
y=${i##*/}
find $i -type f -name "*.md" -exec sed -i.old \
-e '/^<!.*metadata]>/g' \
-e '/^<!.*end-metadata.*>/g' {} \;
;;
*)
y=${i##*/}
find $i -type f -name "*.md" -exec sed -i.old \
-e '/^<!.*metadata]>/g' \
-e '/^<!.*end-metadata.*>/g' \
-e 's/\(\]\)\([(]\)\(\/\)/\1\2\/'$y'\//g' \
-e 's/\(\][(]\)\([A-z].*\)\(\.md\)/\1\/'$y'\/\2/g' \
-e 's/\([(]\)\(.*\)\(\.md\)/\1\2/g' \
-e 's/\(\][(]\)\(\.\/\)/\1\/'$y'\//g' \
-e 's/\(\][(]\)\(\.\.\/\.\.\/\)/\1\/'$y'\//g' \
-e 's/\(\][(]\)\(\.\.\/\)/\1\/'$y'\//g' {} \;
;;
esac
done
#
# Move docker directories to content
#
for i in "${docker_dir[@]}"
do
:
if [ -d $i ]
then
mv $i /docs/content/
fi
done
rm -rf /docs/content/docker

View file

@ -40,8 +40,6 @@ Finally, `docker-compose.yml` is where the magic happens. This file describes th
db: db:
image: postgres image: postgres
ports:
- "5432"
web: web:
build: . build: .
command: bundle exec rails s -p 3000 -b '0.0.0.0' command: bundle exec rails s -p 3000 -b '0.0.0.0'

View file

@ -4,6 +4,7 @@ title = "build"
description = "build" description = "build"
keywords = ["fig, composition, compose, docker, orchestration, cli, build"] keywords = ["fig, composition, compose, docker, orchestration, cli, build"]
[menu.main] [menu.main]
identifier="build.compose"
parent = "smn_compose_cli" parent = "smn_compose_cli"
+++ +++
<![end-metadata]--> <![end-metadata]-->

View file

@ -4,6 +4,7 @@ title = "help"
description = "help" description = "help"
keywords = ["fig, composition, compose, docker, orchestration, cli, help"] keywords = ["fig, composition, compose, docker, orchestration, cli, help"]
[menu.main] [menu.main]
identifier="help.compose"
parent = "smn_compose_cli" parent = "smn_compose_cli"
+++ +++
<![end-metadata]--> <![end-metadata]-->

View file

@ -4,6 +4,7 @@ title = "kill"
description = "Forces running containers to stop." description = "Forces running containers to stop."
keywords = ["fig, composition, compose, docker, orchestration, cli, kill"] keywords = ["fig, composition, compose, docker, orchestration, cli, kill"]
[menu.main] [menu.main]
identifier="kill.compose"
parent = "smn_compose_cli" parent = "smn_compose_cli"
+++ +++
<![end-metadata]--> <![end-metadata]-->

View file

@ -4,6 +4,7 @@ title = "logs"
description = "Displays log output from services." description = "Displays log output from services."
keywords = ["fig, composition, compose, docker, orchestration, cli, logs"] keywords = ["fig, composition, compose, docker, orchestration, cli, logs"]
[menu.main] [menu.main]
identifier="logs.compose"
parent = "smn_compose_cli" parent = "smn_compose_cli"
+++ +++
<![end-metadata]--> <![end-metadata]-->

View file

@ -4,6 +4,7 @@ title = "port"
description = "Prints the public port for a port binding.s" description = "Prints the public port for a port binding.s"
keywords = ["fig, composition, compose, docker, orchestration, cli, port"] keywords = ["fig, composition, compose, docker, orchestration, cli, port"]
[menu.main] [menu.main]
identifier="port.compose"
parent = "smn_compose_cli" parent = "smn_compose_cli"
+++ +++
<![end-metadata]--> <![end-metadata]-->

View file

@ -4,6 +4,7 @@ title = "ps"
description = "Lists containers." description = "Lists containers."
keywords = ["fig, composition, compose, docker, orchestration, cli, ps"] keywords = ["fig, composition, compose, docker, orchestration, cli, ps"]
[menu.main] [menu.main]
identifier="ps.compose"
parent = "smn_compose_cli" parent = "smn_compose_cli"
+++ +++
<![end-metadata]--> <![end-metadata]-->

View file

@ -4,6 +4,7 @@ title = "pull"
description = "Pulls service images." description = "Pulls service images."
keywords = ["fig, composition, compose, docker, orchestration, cli, pull"] keywords = ["fig, composition, compose, docker, orchestration, cli, pull"]
[menu.main] [menu.main]
identifier="pull.compose"
parent = "smn_compose_cli" parent = "smn_compose_cli"
+++ +++
<![end-metadata]--> <![end-metadata]-->

View file

@ -4,6 +4,7 @@ title = "restart"
description = "Restarts Docker Compose services." description = "Restarts Docker Compose services."
keywords = ["fig, composition, compose, docker, orchestration, cli, restart"] keywords = ["fig, composition, compose, docker, orchestration, cli, restart"]
[menu.main] [menu.main]
identifier="restart.compose"
parent = "smn_compose_cli" parent = "smn_compose_cli"
+++ +++
<![end-metadata]--> <![end-metadata]-->

View file

@ -4,6 +4,7 @@ title = "rm"
description = "Removes stopped service containers." description = "Removes stopped service containers."
keywords = ["fig, composition, compose, docker, orchestration, cli, rm"] keywords = ["fig, composition, compose, docker, orchestration, cli, rm"]
[menu.main] [menu.main]
identifier="rm.compose"
parent = "smn_compose_cli" parent = "smn_compose_cli"
+++ +++
<![end-metadata]--> <![end-metadata]-->

View file

@ -4,6 +4,7 @@ title = "run"
description = "Runs a one-off command on a service." description = "Runs a one-off command on a service."
keywords = ["fig, composition, compose, docker, orchestration, cli, run"] keywords = ["fig, composition, compose, docker, orchestration, cli, run"]
[menu.main] [menu.main]
identifier="run.compose"
parent = "smn_compose_cli" parent = "smn_compose_cli"
+++ +++
<![end-metadata]--> <![end-metadata]-->

View file

@ -4,6 +4,7 @@ title = "start"
description = "Starts existing containers for a service." description = "Starts existing containers for a service."
keywords = ["fig, composition, compose, docker, orchestration, cli, start"] keywords = ["fig, composition, compose, docker, orchestration, cli, start"]
[menu.main] [menu.main]
identifier="start.compose"
parent = "smn_compose_cli" parent = "smn_compose_cli"
+++ +++
<![end-metadata]--> <![end-metadata]-->

View file

@ -4,6 +4,7 @@ title = "stop"
description = "Stops running containers without removing them. " description = "Stops running containers without removing them. "
keywords = ["fig, composition, compose, docker, orchestration, cli, stop"] keywords = ["fig, composition, compose, docker, orchestration, cli, stop"]
[menu.main] [menu.main]
identifier="stop.compose"
parent = "smn_compose_cli" parent = "smn_compose_cli"
+++ +++
<![end-metadata]--> <![end-metadata]-->

View file

@ -4,6 +4,7 @@ title = "up"
description = "Builds, (re)creates, starts, and attaches to containers for a service." description = "Builds, (re)creates, starts, and attaches to containers for a service."
keywords = ["fig, composition, compose, docker, orchestration, cli, up"] keywords = ["fig, composition, compose, docker, orchestration, cli, up"]
[menu.main] [menu.main]
identifier="up.compose"
parent = "smn_compose_cli" parent = "smn_compose_cli"
+++ +++
<![end-metadata]--> <![end-metadata]-->

View file

@ -131,9 +131,16 @@ Mount paths as volumes, optionally specifying a path on the host machine
volumes: volumes:
- /var/lib/mysql - /var/lib/mysql
- cache/:/tmp/cache - ./cache:/tmp/cache
- ~/configs:/etc/configs/:ro - ~/configs:/etc/configs/:ro
You can mount a relative path on the host, which will expand relative to
the directory of the Compose configuration file being used. Relative paths
should always begin with `.` or `..`.
> Note: No path expansion will be done if you have also specified a
> `volume_driver`.
### volumes_from ### volumes_from
Mount all of the volumes from another service or container. Mount all of the volumes from another service or container.
@ -333,7 +340,7 @@ Override the default labeling scheme for each container.
- label:user:USER - label:user:USER
- label:role:ROLE - label:role:ROLE
### working\_dir, entrypoint, user, hostname, domainname, mac\_address, mem\_limit, memswap\_limit, privileged, restart, stdin\_open, tty, cpu\_shares, cpuset, read\_only ### working\_dir, entrypoint, user, hostname, domainname, mac\_address, mem\_limit, memswap\_limit, privileged, restart, stdin\_open, tty, cpu\_shares, cpuset, read\_only, volume\_driver
Each of these is a single value, analogous to its Each of these is a single value, analogous to its
[docker run](https://docs.docker.com/reference/run/) counterpart. [docker run](https://docs.docker.com/reference/run/) counterpart.
@ -360,6 +367,8 @@ Each of these is a single value, analogous to its
tty: true tty: true
read_only: true read_only: true
volume_driver: mydriver
```
## Compose documentation ## Compose documentation

View file

@ -7,10 +7,17 @@ fi
# If a pidfile is still around (for example after a container restart), # If a pidfile is still around (for example after a container restart),
# delete it so that docker can start. # delete it so that docker can start.
rm -rf /var/run/docker.pid rm -rf /var/run/docker.pid
docker -d $DOCKER_DAEMON_ARGS &>/var/log/docker.log & docker -d --storage-driver="overlay" &>/var/log/docker.log &
docker_pid=$!
>&2 echo "Waiting for Docker to start..." >&2 echo "Waiting for Docker to start..."
while ! docker ps &>/dev/null; do while ! docker ps &>/dev/null; do
if ! kill -0 "$docker_pid" &>/dev/null; then
>&2 echo "Docker failed to start"
cat /var/log/docker.log
exit 1
fi
sleep 1 sleep 1
done done

View file

@ -1,5 +1,7 @@
import sys import sys
import mock # noqa
if sys.version_info >= (2, 7): if sys.version_info >= (2, 7):
import unittest # NOQA import unittest # NOQA
else: else:

View file

@ -4,6 +4,7 @@ import sys
import os import os
import shlex import shlex
import mock
from six import StringIO from six import StringIO
from mock import patch from mock import patch
@ -104,7 +105,7 @@ class CLITestCase(DockerClientTestCase):
output = mock_stdout.getvalue() output = mock_stdout.getvalue()
self.assertNotIn(cache_indicator, output) self.assertNotIn(cache_indicator, output)
def test_up(self): def test_up_detached(self):
self.command.dispatch(['up', '-d'], None) self.command.dispatch(['up', '-d'], None)
service = self.project.get_service('simple') service = self.project.get_service('simple')
another = self.project.get_service('another') another = self.project.get_service('another')
@ -112,10 +113,28 @@ class CLITestCase(DockerClientTestCase):
self.assertEqual(len(another.containers()), 1) self.assertEqual(len(another.containers()), 1)
# Ensure containers don't have stdin and stdout connected in -d mode # Ensure containers don't have stdin and stdout connected in -d mode
config = service.containers()[0].inspect()['Config'] container, = service.containers()
self.assertFalse(config['AttachStderr']) self.assertFalse(container.get('Config.AttachStderr'))
self.assertFalse(config['AttachStdout']) self.assertFalse(container.get('Config.AttachStdout'))
self.assertFalse(config['AttachStdin']) self.assertFalse(container.get('Config.AttachStdin'))
def test_up_attached(self):
with mock.patch(
'compose.cli.main.attach_to_logs',
autospec=True
) as mock_attach:
self.command.dispatch(['up'], None)
_, args, kwargs = mock_attach.mock_calls[0]
_project, log_printer, _names, _timeout = args
service = self.project.get_service('simple')
another = self.project.get_service('another')
self.assertEqual(len(service.containers()), 1)
self.assertEqual(len(another.containers()), 1)
self.assertEqual(
set(log_printer.containers),
set(self.project.containers())
)
def test_up_with_links(self): def test_up_with_links(self):
self.command.base_dir = 'tests/fixtures/links-composefile' self.command.base_dir = 'tests/fixtures/links-composefile'

View file

@ -65,7 +65,7 @@ class UtilitiesTestCase(unittest.TestCase):
legacy.is_valid_name("composetest_web_lol_1", one_off=True), legacy.is_valid_name("composetest_web_lol_1", one_off=True),
) )
def test_get_legacy_containers_no_labels(self): def test_get_legacy_containers(self):
client = Mock() client = Mock()
client.containers.return_value = [ client.containers.return_value = [
{ {
@ -74,12 +74,23 @@ class UtilitiesTestCase(unittest.TestCase):
"Name": "composetest_web_1", "Name": "composetest_web_1",
"Labels": None, "Labels": None,
}, },
{
"Id": "ghi789",
"Image": "def456",
"Name": None,
"Labels": None,
},
{
"Id": "jkl012",
"Image": "def456",
"Labels": None,
},
] ]
containers = list(legacy.get_legacy_containers( containers = legacy.get_legacy_containers(client, "composetest", ["web"])
client, "composetest", ["web"]))
self.assertEqual(len(containers), 1) self.assertEqual(len(containers), 1)
self.assertEqual(containers[0].id, 'abc123')
class LegacyTestCase(DockerClientTestCase): class LegacyTestCase(DockerClientTestCase):

View file

@ -112,7 +112,7 @@ class ProjectTest(DockerClientTestCase):
web = project.get_service('web') web = project.get_service('web')
net = project.get_service('net') net = project.get_service('net')
self.assertEqual(web._get_net(), 'container:' + net.containers()[0].id) self.assertEqual(web.net.mode, 'container:' + net.containers()[0].id)
def test_net_from_container(self): def test_net_from_container(self):
net_container = Container.create( net_container = Container.create(
@ -138,7 +138,7 @@ class ProjectTest(DockerClientTestCase):
project.up() project.up()
web = project.get_service('web') web = project.get_service('web')
self.assertEqual(web._get_net(), 'container:' + net_container.id) self.assertEqual(web.net.mode, 'container:' + net_container.id)
def test_start_stop_kill_remove(self): def test_start_stop_kill_remove(self):
web = self.create_service('web') web = self.create_service('web')

View file

@ -9,6 +9,8 @@ import tempfile
import shutil import shutil
from six import StringIO, text_type from six import StringIO, text_type
from .testcases import DockerClientTestCase
from .testcases import pull_busybox
from compose import __version__ from compose import __version__
from compose.const import ( from compose.const import (
LABEL_CONTAINER_NUMBER, LABEL_CONTAINER_NUMBER,
@ -17,14 +19,12 @@ from compose.const import (
LABEL_SERVICE, LABEL_SERVICE,
LABEL_VERSION, LABEL_VERSION,
) )
from compose.service import (
ConfigError,
ConvergencePlan,
Service,
build_extra_hosts,
)
from compose.container import Container from compose.container import Container
from .testcases import DockerClientTestCase from compose.service import build_extra_hosts
from compose.service import ConfigError
from compose.service import ConvergencePlan
from compose.service import Net
from compose.service import Service
def create_and_start_container(service, **override_options): def create_and_start_container(service, **override_options):
@ -117,11 +117,17 @@ class ServiceTest(DockerClientTestCase):
service.start_container(container) service.start_container(container)
self.assertIn('/var/db', container.get('Volumes')) self.assertIn('/var/db', container.get('Volumes'))
def test_create_container_with_volume_driver(self):
service = self.create_service('db', volume_driver='foodriver')
container = service.create_container()
service.start_container(container)
self.assertEqual('foodriver', container.get('Config.VolumeDriver'))
def test_create_container_with_cpu_shares(self): def test_create_container_with_cpu_shares(self):
service = self.create_service('db', cpu_shares=73) service = self.create_service('db', cpu_shares=73)
container = service.create_container() container = service.create_container()
service.start_container(container) service.start_container(container)
self.assertEqual(container.inspect()['Config']['CpuShares'], 73) self.assertEqual(container.get('HostConfig.CpuShares'), 73)
def test_build_extra_hosts(self): def test_build_extra_hosts(self):
# string # string
@ -183,7 +189,7 @@ class ServiceTest(DockerClientTestCase):
service = self.create_service('db', cpuset='0') service = self.create_service('db', cpuset='0')
container = service.create_container() container = service.create_container()
service.start_container(container) service.start_container(container)
self.assertEqual(container.inspect()['Config']['Cpuset'], '0') self.assertEqual(container.get('HostConfig.CpusetCpus'), '0')
def test_create_container_with_read_only_root_fs(self): def test_create_container_with_read_only_root_fs(self):
read_only = True read_only = True
@ -221,6 +227,52 @@ class ServiceTest(DockerClientTestCase):
self.assertTrue(path.basename(actual_host_path) == path.basename(host_path), self.assertTrue(path.basename(actual_host_path) == path.basename(host_path),
msg=("Last component differs: %s, %s" % (actual_host_path, host_path))) msg=("Last component differs: %s, %s" % (actual_host_path, host_path)))
def test_recreate_preserves_volume_with_trailing_slash(self):
"""
When the Compose file specifies a trailing slash in the container path, make
sure we copy the volume over when recreating.
"""
service = self.create_service('data', volumes=['/data/'])
old_container = create_and_start_container(service)
volume_path = old_container.get('Volumes')['/data']
new_container = service.recreate_container(old_container)
self.assertEqual(new_container.get('Volumes')['/data'], volume_path)
def test_duplicate_volume_trailing_slash(self):
"""
When an image specifies a volume, and the Compose file specifies a host path
but adds a trailing slash, make sure that we don't create duplicate binds.
"""
host_path = '/tmp/data'
container_path = '/data'
volumes = ['{}:{}/'.format(host_path, container_path)]
tmp_container = self.client.create_container(
'busybox', 'true',
volumes={container_path: {}},
labels={'com.docker.compose.test_image': 'true'},
)
image = self.client.commit(tmp_container)['Id']
service = self.create_service('db', image=image, volumes=volumes)
old_container = create_and_start_container(service)
self.assertEqual(
old_container.get('Config.Volumes'),
{container_path: {}},
)
service = self.create_service('db', image=image, volumes=volumes)
new_container = service.recreate_container(old_container)
self.assertEqual(
new_container.get('Config.Volumes'),
{container_path: {}},
)
self.assertEqual(service.containers(stopped=False), [new_container])
@patch.dict(os.environ) @patch.dict(os.environ)
def test_create_container_with_home_and_env_var_in_volume_path(self): def test_create_container_with_home_and_env_var_in_volume_path(self):
os.environ['VOLUME_NAME'] = 'my-volume' os.environ['VOLUME_NAME'] = 'my-volume'
@ -526,8 +578,10 @@ class ServiceTest(DockerClientTestCase):
}) })
def test_create_with_image_id(self): def test_create_with_image_id(self):
# Image id for the current busybox:latest # Get image id for the current busybox:latest
service = self.create_service('foo', image='8c2e06607696') pull_busybox(self.client)
image_id = self.client.inspect_image('busybox:latest')['Id'][:12]
service = self.create_service('foo', image=image_id)
service.create_container() service.create_container()
def test_scale(self): def test_scale(self):
@ -620,6 +674,25 @@ class ServiceTest(DockerClientTestCase):
self.assertTrue(service.containers()[0].is_running) self.assertTrue(service.containers()[0].is_running)
self.assertIn("ERROR: for 2 Boom", mock_stdout.getvalue()) self.assertIn("ERROR: for 2 Boom", mock_stdout.getvalue())
@patch('sys.stdout', new_callable=StringIO)
def test_scale_with_api_returns_unexpected_exception(self, mock_stdout):
"""
Test that when scaling if the API returns an error, that is not of type
APIError, that error is re-raised.
"""
service = self.create_service('web')
next_number = service._next_container_number()
service.create_container(number=next_number, quiet=True)
with patch(
'compose.container.Container.create',
side_effect=ValueError("BOOM")):
with self.assertRaises(ValueError):
service.scale(3)
self.assertEqual(len(service.containers()), 1)
self.assertTrue(service.containers()[0].is_running)
@patch('compose.service.log') @patch('compose.service.log')
def test_scale_with_desired_number_already_achieved(self, mock_log): def test_scale_with_desired_number_already_achieved(self, mock_log):
""" """
@ -672,17 +745,17 @@ class ServiceTest(DockerClientTestCase):
self.assertEqual(list(container.inspect()['HostConfig']['PortBindings'].keys()), ['8000/tcp']) self.assertEqual(list(container.inspect()['HostConfig']['PortBindings'].keys()), ['8000/tcp'])
def test_network_mode_none(self): def test_network_mode_none(self):
service = self.create_service('web', net='none') service = self.create_service('web', net=Net('none'))
container = create_and_start_container(service) container = create_and_start_container(service)
self.assertEqual(container.get('HostConfig.NetworkMode'), 'none') self.assertEqual(container.get('HostConfig.NetworkMode'), 'none')
def test_network_mode_bridged(self): def test_network_mode_bridged(self):
service = self.create_service('web', net='bridge') service = self.create_service('web', net=Net('bridge'))
container = create_and_start_container(service) container = create_and_start_container(service)
self.assertEqual(container.get('HostConfig.NetworkMode'), 'bridge') self.assertEqual(container.get('HostConfig.NetworkMode'), 'bridge')
def test_network_mode_host(self): def test_network_mode_host(self):
service = self.create_service('web', net='host') service = self.create_service('web', net=Net('host'))
container = create_and_start_container(service) container = create_and_start_container(service)
self.assertEqual(container.get('HostConfig.NetworkMode'), 'host') self.assertEqual(container.get('HostConfig.NetworkMode'), 'host')

View file

@ -1,3 +1,7 @@
"""
Integration tests which cover state convergence (aka smart recreate) performed
by `docker-compose up`.
"""
from __future__ import unicode_literals from __future__ import unicode_literals
import tempfile import tempfile
import shutil import shutil
@ -151,6 +155,24 @@ class ProjectWithDependenciesTest(ProjectTestCase):
self.assertEqual(new_containers - old_containers, set()) self.assertEqual(new_containers - old_containers, set())
def test_service_removed_while_down(self):
next_cfg = {
'web': {
'image': 'busybox:latest',
'command': 'tail -f /dev/null',
},
'nginx': self.cfg['nginx'],
}
containers = self.run_up(self.cfg)
self.assertEqual(len(containers), 3)
project = self.make_project(self.cfg)
project.stop(timeout=1)
containers = self.run_up(next_cfg)
self.assertEqual(len(containers), 2)
def converge(service, def converge(service,
allow_recreate=True, allow_recreate=True,

View file

@ -1,5 +1,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from __future__ import absolute_import from __future__ import absolute_import
from docker import errors
from compose.service import Service from compose.service import Service
from compose.config import ServiceLoader from compose.config import ServiceLoader
from compose.const import LABEL_PROJECT from compose.const import LABEL_PROJECT
@ -8,6 +11,13 @@ from compose.progress_stream import stream_output
from .. import unittest from .. import unittest
def pull_busybox(client):
try:
client.inspect_image('busybox:latest')
except errors.APIError:
client.pull('busybox:latest', stream=False)
class DockerClientTestCase(unittest.TestCase): class DockerClientTestCase(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):

View file

@ -0,0 +1,57 @@
from __future__ import absolute_import
from compose import container
from compose.cli.log_printer import LogPrinter
from compose.cli.main import attach_to_logs
from compose.cli.main import build_log_printer
from compose.project import Project
from tests import mock
from tests import unittest
def mock_container(service, number):
return mock.create_autospec(
container.Container,
service=service,
number=number,
name_without_project='{0}_{1}'.format(service, number))
class CLIMainTestCase(unittest.TestCase):
def test_build_log_printer(self):
containers = [
mock_container('web', 1),
mock_container('web', 2),
mock_container('db', 1),
mock_container('other', 1),
mock_container('another', 1),
]
service_names = ['web', 'db']
log_printer = build_log_printer(containers, service_names, True)
self.assertEqual(log_printer.containers, containers[:3])
def test_build_log_printer_all_services(self):
containers = [
mock_container('web', 1),
mock_container('db', 1),
mock_container('other', 1),
]
service_names = []
log_printer = build_log_printer(containers, service_names, True)
self.assertEqual(log_printer.containers, containers)
def test_attach_to_logs(self):
project = mock.create_autospec(Project)
log_printer = mock.create_autospec(LogPrinter, containers=[])
service_names = ['web', 'db']
timeout = 12
with mock.patch('compose.cli.main.signal', autospec=True) as mock_signal:
attach_to_logs(project, log_printer, service_names, timeout)
mock_signal.signal.assert_called_once_with(mock_signal.SIGINT, mock.ANY)
log_printer.run.assert_called_once_with()
project.stop.assert_called_once_with(
service_names=service_names,
timeout=timeout)

View file

@ -9,7 +9,7 @@ from compose import config
def make_service_dict(name, service_dict, working_dir): def make_service_dict(name, service_dict, working_dir):
""" """
Test helper function to contruct a ServiceLoader Test helper function to construct a ServiceLoader
""" """
return config.ServiceLoader(working_dir=working_dir).make_service_dict(name, service_dict) return config.ServiceLoader(working_dir=working_dir).make_service_dict(name, service_dict)
@ -72,6 +72,67 @@ class VolumePathTest(unittest.TestCase):
d = make_service_dict('foo', {'volumes': ['~:/container/path']}, working_dir='.') d = make_service_dict('foo', {'volumes': ['~:/container/path']}, working_dir='.')
self.assertEqual(d['volumes'], ['/home/user:/container/path']) self.assertEqual(d['volumes'], ['/home/user:/container/path'])
@mock.patch.dict(os.environ)
def test_volume_binding_with_local_dir_name_raises_warning(self):
def make_dict(**config):
make_service_dict('foo', config, working_dir='.')
with mock.patch('compose.config.log.warn') as warn:
make_dict(volumes=['/container/path'])
self.assertEqual(0, warn.call_count)
make_dict(volumes=['/data:/container/path'])
self.assertEqual(0, warn.call_count)
make_dict(volumes=['.:/container/path'])
self.assertEqual(0, warn.call_count)
make_dict(volumes=['..:/container/path'])
self.assertEqual(0, warn.call_count)
make_dict(volumes=['./data:/container/path'])
self.assertEqual(0, warn.call_count)
make_dict(volumes=['../data:/container/path'])
self.assertEqual(0, warn.call_count)
make_dict(volumes=['.profile:/container/path'])
self.assertEqual(0, warn.call_count)
make_dict(volumes=['~:/container/path'])
self.assertEqual(0, warn.call_count)
make_dict(volumes=['~/data:/container/path'])
self.assertEqual(0, warn.call_count)
make_dict(volumes=['~tmp:/container/path'])
self.assertEqual(0, warn.call_count)
make_dict(volumes=['data:/container/path'], volume_driver='mydriver')
self.assertEqual(0, warn.call_count)
make_dict(volumes=['data:/container/path'])
self.assertEqual(1, warn.call_count)
warning = warn.call_args[0][0]
self.assertIn('"data:/container/path"', warning)
self.assertIn('"./data:/container/path"', warning)
def test_named_volume_with_driver_does_not_expand(self):
d = make_service_dict('foo', {
'volumes': ['namedvolume:/data'],
'volume_driver': 'foodriver',
}, working_dir='.')
self.assertEqual(d['volumes'], ['namedvolume:/data'])
@mock.patch.dict(os.environ)
def test_named_volume_with_special_chars(self):
os.environ['NAME'] = 'surprise!'
d = make_service_dict('foo', {
'volumes': ['~/${NAME}:/data'],
'volume_driver': 'foodriver',
}, working_dir='.')
self.assertEqual(d['volumes'], ['~/${NAME}:/data'])
class MergePathMappingTest(object): class MergePathMappingTest(object):
def config_name(self): def config_name(self):
@ -451,7 +512,7 @@ class ExtendsTest(unittest.TestCase):
We specify a 'file' key that is the filename we're already in. We specify a 'file' key that is the filename we're already in.
""" """
service_dicts = load_from_filename('tests/fixtures/extends/specify-file-as-self.yml') service_dicts = load_from_filename('tests/fixtures/extends/specify-file-as-self.yml')
self.assertEqual(service_dicts, [ self.assertEqual(sorted(service_dicts), sorted([
{ {
'environment': 'environment':
{ {
@ -471,7 +532,7 @@ class ExtendsTest(unittest.TestCase):
'image': 'busybox', 'image': 'busybox',
'name': 'web' 'name': 'web'
} }
]) ]))
def test_circular(self): def test_circular(self):
try: try:

View file

@ -3,6 +3,7 @@ from .. import unittest
from compose.service import Service from compose.service import Service
from compose.project import Project from compose.project import Project
from compose.container import Container from compose.container import Container
from compose.const import LABEL_SERVICE
import mock import mock
import docker import docker
@ -219,7 +220,7 @@ class ProjectTest(unittest.TestCase):
} }
], self.mock_client) ], self.mock_client)
service = project.get_service('test') service = project.get_service('test')
self.assertEqual(service._get_net(), None) self.assertEqual(service.net.id, None)
self.assertNotIn('NetworkMode', service._get_container_host_config({})) self.assertNotIn('NetworkMode', service._get_container_host_config({}))
def test_use_net_from_container(self): def test_use_net_from_container(self):
@ -234,7 +235,7 @@ class ProjectTest(unittest.TestCase):
} }
], self.mock_client) ], self.mock_client)
service = project.get_service('test') service = project.get_service('test')
self.assertEqual(service._get_net(), 'container:' + container_id) self.assertEqual(service.net.mode, 'container:' + container_id)
def test_use_net_from_service(self): def test_use_net_from_service(self):
container_name = 'test_aaa_1' container_name = 'test_aaa_1'
@ -259,4 +260,28 @@ class ProjectTest(unittest.TestCase):
], self.mock_client) ], self.mock_client)
service = project.get_service('test') service = project.get_service('test')
self.assertEqual(service._get_net(), 'container:' + container_name) self.assertEqual(service.net.mode, 'container:' + container_name)
def test_container_without_name(self):
self.mock_client.containers.return_value = [
{'Image': 'busybox:latest', 'Id': '1', 'Name': '1'},
{'Image': 'busybox:latest', 'Id': '2', 'Name': None},
{'Image': 'busybox:latest', 'Id': '3'},
]
self.mock_client.inspect_container.return_value = {
'Id': '1',
'Config': {
'Labels': {
LABEL_SERVICE: 'web',
},
},
}
project = Project.from_dicts(
'test',
[{
'name': 'web',
'image': 'busybox:latest',
}],
self.mock_client,
)
self.assertEqual([c.id for c in project.containers()], ['1'])

View file

@ -7,21 +7,25 @@ import mock
import docker import docker
from docker.utils import LogConfig from docker.utils import LogConfig
from compose.service import Service from compose.const import LABEL_CONFIG_HASH
from compose.const import LABEL_ONE_OFF
from compose.const import LABEL_PROJECT
from compose.const import LABEL_SERVICE
from compose.container import Container from compose.container import Container
from compose.const import LABEL_SERVICE, LABEL_PROJECT, LABEL_ONE_OFF from compose.service import ConfigError
from compose.service import ( from compose.service import ContainerNet
ConfigError, from compose.service import NeedsBuildError
NeedsBuildError, from compose.service import Net
NoSuchImageError, from compose.service import NoSuchImageError
build_port_bindings, from compose.service import Service
build_volume_binding, from compose.service import ServiceNet
get_container_data_volumes, from compose.service import build_port_bindings
merge_volume_bindings, from compose.service import build_volume_binding
parse_repository_tag, from compose.service import get_container_data_volumes
parse_volume_spec, from compose.service import merge_volume_bindings
split_port, from compose.service import parse_repository_tag
) from compose.service import parse_volume_spec
from compose.service import split_port
class ServiceTest(unittest.TestCase): class ServiceTest(unittest.TestCase):
@ -76,6 +80,18 @@ class ServiceTest(unittest.TestCase):
all=False, all=False,
filters={'label': expected_labels}) filters={'label': expected_labels})
def test_container_without_name(self):
self.mock_client.containers.return_value = [
{'Image': 'foo', 'Id': '1', 'Name': '1'},
{'Image': 'foo', 'Id': '2', 'Name': None},
{'Image': 'foo', 'Id': '3'},
]
service = Service('db', self.mock_client, 'myproject', image='foo')
self.assertEqual([c.id for c in service.containers()], ['1'])
self.assertEqual(service._next_container_number(), 2)
self.assertEqual(service.get_container(1).id, '1')
def test_get_volumes_from_container(self): def test_get_volumes_from_container(self):
container_id = 'aabbccddee' container_id = 'aabbccddee'
service = Service( service = Service(
@ -161,8 +177,8 @@ class ServiceTest(unittest.TestCase):
service = Service(name='foo', image='foo', hostname='name', client=self.mock_client, mem_limit=1000000000, memswap_limit=2000000000) service = Service(name='foo', image='foo', hostname='name', client=self.mock_client, mem_limit=1000000000, memswap_limit=2000000000)
self.mock_client.containers.return_value = [] self.mock_client.containers.return_value = []
opts = service._get_container_create_options({'some': 'overrides'}, 1) opts = service._get_container_create_options({'some': 'overrides'}, 1)
self.assertEqual(opts['memswap_limit'], 2000000000) self.assertEqual(opts['host_config']['MemorySwap'], 2000000000)
self.assertEqual(opts['mem_limit'], 1000000000) self.assertEqual(opts['host_config']['Memory'], 1000000000)
def test_log_opt(self): def test_log_opt(self):
log_opt = {'address': 'tcp://192.168.0.42:123'} log_opt = {'address': 'tcp://192.168.0.42:123'}
@ -209,6 +225,40 @@ class ServiceTest(unittest.TestCase):
self.assertEqual(opts['hostname'], 'name.sub', 'hostname') self.assertEqual(opts['hostname'], 'name.sub', 'hostname')
self.assertEqual(opts['domainname'], 'domain.tld', 'domainname') self.assertEqual(opts['domainname'], 'domain.tld', 'domainname')
def test_get_container_create_options_does_not_mutate_options(self):
labels = {'thing': 'real'}
environment = {'also': 'real'}
service = Service(
'foo',
image='foo',
labels=dict(labels),
client=self.mock_client,
environment=dict(environment),
)
self.mock_client.inspect_image.return_value = {'Id': 'abcd'}
prev_container = mock.Mock(
id='ababab',
image_config={'ContainerConfig': {}})
opts = service._get_container_create_options(
{},
1,
previous_container=prev_container)
self.assertEqual(service.options['labels'], labels)
self.assertEqual(service.options['environment'], environment)
self.assertEqual(
opts['labels'][LABEL_CONFIG_HASH],
'3c85881a8903b9d73a06c41860c8be08acce1494ab4cf8408375966dccd714de')
self.assertEqual(
opts['environment'],
{
'affinity:container': '=ababab',
'also': 'real',
}
)
def test_get_container_not_found(self): def test_get_container_not_found(self):
self.mock_client.containers.return_value = [] self.mock_client.containers.return_value = []
service = Service('foo', client=self.mock_client, image='foo') service = Service('foo', client=self.mock_client, image='foo')
@ -328,6 +378,90 @@ class ServiceTest(unittest.TestCase):
self.assertEqual(self.mock_client.build.call_count, 1) self.assertEqual(self.mock_client.build.call_count, 1)
self.assertFalse(self.mock_client.build.call_args[1]['pull']) self.assertFalse(self.mock_client.build.call_args[1]['pull'])
def test_config_dict(self):
self.mock_client.inspect_image.return_value = {'Id': 'abcd'}
service = Service(
'foo',
image='example.com/foo',
client=self.mock_client,
net=ServiceNet(Service('other', image='foo')),
links=[(Service('one', image='foo'), 'one')],
volumes_from=[Service('two', image='foo')])
config_dict = service.config_dict()
expected = {
'image_id': 'abcd',
'options': {'image': 'example.com/foo'},
'links': [('one', 'one')],
'net': 'other',
'volumes_from': ['two'],
}
self.assertEqual(config_dict, expected)
def test_config_dict_with_net_from_container(self):
self.mock_client.inspect_image.return_value = {'Id': 'abcd'}
container = Container(
self.mock_client,
{'Id': 'aaabbb', 'Name': '/foo_1'})
service = Service(
'foo',
image='example.com/foo',
client=self.mock_client,
net=container)
config_dict = service.config_dict()
expected = {
'image_id': 'abcd',
'options': {'image': 'example.com/foo'},
'links': [],
'net': 'aaabbb',
'volumes_from': [],
}
self.assertEqual(config_dict, expected)
class NetTestCase(unittest.TestCase):
def test_net(self):
net = Net('host')
self.assertEqual(net.id, 'host')
self.assertEqual(net.mode, 'host')
self.assertEqual(net.service_name, None)
def test_net_container(self):
container_id = 'abcd'
net = ContainerNet(Container(None, {'Id': container_id}))
self.assertEqual(net.id, container_id)
self.assertEqual(net.mode, 'container:' + container_id)
self.assertEqual(net.service_name, None)
def test_net_service(self):
container_id = 'bbbb'
service_name = 'web'
mock_client = mock.create_autospec(docker.Client)
mock_client.containers.return_value = [
{'Id': container_id, 'Name': container_id, 'Image': 'abcd'},
]
service = Service(name=service_name, client=mock_client, image='foo')
net = ServiceNet(service)
self.assertEqual(net.id, service_name)
self.assertEqual(net.mode, 'container:' + container_id)
self.assertEqual(net.service_name, service_name)
def test_net_service_no_containers(self):
service_name = 'web'
mock_client = mock.create_autospec(docker.Client)
mock_client.containers.return_value = []
service = Service(name=service_name, client=mock_client, image='foo')
net = ServiceNet(service)
self.assertEqual(net.id, service_name)
self.assertEqual(net.mode, None)
self.assertEqual(net.service_name, service_name)
def mock_get_image(images): def mock_get_image(images):
if images: if images: