Compare commits
388 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a75c16cb1b | ||
|
|
e08409f18d | ||
|
|
9d7dbe3857 | ||
|
|
bcd5286cd3 | ||
|
|
daebf74d6c | ||
|
|
155d813606 | ||
|
|
73c2f8ee37 | ||
|
|
4aae2c3b7b | ||
|
|
b79ad5f966 | ||
|
|
f8e3c46fbb | ||
|
|
f7c923062d | ||
|
|
61906ac2ff | ||
|
|
ea8032c115 | ||
|
|
7d22809ef4 | ||
|
|
cfda9d844e | ||
|
|
1e29ad9fc7 | ||
|
|
fcf78fe3de | ||
|
|
8d7b1e9047 | ||
|
|
6d2aa80435 | ||
|
|
13ec3d0217 | ||
|
|
f59fef09a6 | ||
|
|
f0a8c65b05 | ||
|
|
674e541cf7 | ||
|
|
ed5fedf516 | ||
|
|
654b3710f7 | ||
|
|
f4a22b94ed | ||
|
|
e5689afe4c | ||
|
|
42cb719b52 | ||
|
|
6ac6860dda | ||
|
|
353da73eab | ||
|
|
c686be8fd3 | ||
|
|
2f7a77e954 | ||
|
|
d78ea85301 | ||
|
|
7c95c733a9 | ||
|
|
dd9a8d6eee | ||
|
|
4f7c950ca8 | ||
|
|
a1d6e3b9e3 | ||
|
|
3eac70a9d3 | ||
|
|
c7687592ff | ||
|
|
155efd28fa | ||
|
|
2ced83e3d9 | ||
|
|
cea7911f56 | ||
|
|
db12794b1c | ||
|
|
deeca57a0d | ||
|
|
edcbe2eb4d | ||
|
|
ad00f3dd21 | ||
|
|
c77a8cfe3b | ||
|
|
8548b75582 | ||
|
|
c1be49ad53 | ||
|
|
a716bdc482 | ||
|
|
abec6f5891 | ||
|
|
c195915263 | ||
|
|
34d8f9b55a | ||
|
|
f5533c1ed8 | ||
|
|
722e3a2fc7 | ||
|
|
9503aa2b5f | ||
|
|
d99cad60e7 | ||
|
|
8199c4a6e1 | ||
|
|
19ae76a442 | ||
|
|
a55210413c | ||
|
|
6caa188730 | ||
|
|
413a55aa71 | ||
|
|
64336615cf | ||
|
|
a59982eb11 | ||
|
|
46f33f12b0 | ||
|
|
dc718eae65 | ||
|
|
aa5ff05463 | ||
|
|
3b7471ae84 | ||
|
|
7f009aeeb9 | ||
|
|
44c7d080bd | ||
|
|
32bd760526 | ||
|
|
009dbbe971 | ||
|
|
6b59ba0c31 | ||
|
|
61d00ebee4 | ||
|
|
3b1276bd44 | ||
|
|
3e8a4a5dc3 | ||
|
|
0c87e0b18f | ||
|
|
8024f2f09e | ||
|
|
f081376067 | ||
|
|
ce0d469c18 | ||
|
|
b2ee08f439 | ||
|
|
e9ba06ed4b | ||
|
|
9e9b36460c | ||
|
|
25df0d8147 | ||
|
|
c98c617c30 | ||
|
|
d3cd9213c1 | ||
|
|
bbaae11a0f | ||
|
|
e925b8272b | ||
|
|
110401b6f0 | ||
|
|
8fb90bd732 | ||
|
|
24e71db345 | ||
|
|
fbe8484377 | ||
|
|
89cca7bcb2 | ||
|
|
be66779fe9 | ||
|
|
1772909fe2 | ||
|
|
86bdab64ab | ||
|
|
36a10f8dd5 | ||
|
|
9249ec62c2 | ||
|
|
4537ec70cc | ||
|
|
695c692be6 | ||
|
|
297d20f085 | ||
|
|
16ef3d0eb8 | ||
|
|
d765a3fb91 | ||
|
|
7f4a94514b | ||
|
|
c0fe545947 | ||
|
|
52e74ab7ad | ||
|
|
2b46685855 | ||
|
|
2b7306967b | ||
|
|
3f28472ebc | ||
|
|
3da25aa463 | ||
|
|
a66bf72199 | ||
|
|
883227c4d8 | ||
|
|
5545c55ecc | ||
|
|
227fa5c0de | ||
|
|
d399b7893f | ||
|
|
833e16117e | ||
|
|
bf068a8287 | ||
|
|
b1ebf5ce17 | ||
|
|
836ec70979 | ||
|
|
6ca410fd6b | ||
|
|
38a6d04852 | ||
|
|
0952c1bb51 | ||
|
|
18a1829db0 | ||
|
|
e40de088f3 | ||
|
|
6fe54f5c24 | ||
|
|
5cfd947f38 | ||
|
|
513a6b35cc | ||
|
|
cbec6f8834 | ||
|
|
76bc06b729 | ||
|
|
c9ef1fa32f | ||
|
|
c52eed66b7 | ||
|
|
3f65bdcf46 | ||
|
|
6e5c312768 | ||
|
|
33bb7c4e02 | ||
|
|
bb377d3fe6 | ||
|
|
7442b416e8 | ||
|
|
fbee4ce4b3 | ||
|
|
8398382b65 | ||
|
|
7dd5fd5763 | ||
|
|
d5f3826ec7 | ||
|
|
20c936a251 | ||
|
|
d2556a1347 | ||
|
|
237f134a00 | ||
|
|
d4720f85ef | ||
|
|
64fc2b85cb | ||
|
|
a22d248390 | ||
|
|
fffedfc87b | ||
|
|
34fd042dbf | ||
|
|
89e31f7a8d | ||
|
|
b446c09735 | ||
|
|
8f48fa4747 | ||
|
|
7240ff35ee | ||
|
|
aaf66e3485 | ||
|
|
96f4a42a35 | ||
|
|
e6fbca42a1 | ||
|
|
527bf3b023 | ||
|
|
ab36c9c6cd | ||
|
|
e67419065a | ||
|
|
69e956ce8b | ||
|
|
0dbd99bad2 | ||
|
|
fa975d7fbe | ||
|
|
81f0e72bd2 | ||
|
|
da27f8e7e2 | ||
|
|
8572d50903 | ||
|
|
5d39813e1b | ||
|
|
b19315b57e | ||
|
|
e549875e89 | ||
|
|
7e21b05f05 | ||
|
|
bea2072b95 | ||
|
|
3b6cc7a7bb | ||
|
|
a264470cc0 | ||
|
|
844e2c3d26 | ||
|
|
210a14cf28 | ||
|
|
9ce4024951 | ||
|
|
8fb6fb7b19 | ||
|
|
83760d0e9e | ||
|
|
be5b7b6f0e | ||
|
|
e5a02d3052 | ||
|
|
3a395892fc | ||
|
|
09f6a876cf | ||
|
|
0117148a36 | ||
|
|
8f70c8cdeb | ||
|
|
16a74f3797 | ||
|
|
c42918ec7c | ||
|
|
d28b2027b8 | ||
|
|
8d816fc2f3 | ||
|
|
f476436027 | ||
|
|
fae20305ec | ||
|
|
4628e93fb2 | ||
|
|
82086a4e92 | ||
|
|
96e9b47059 | ||
|
|
34166ef5a4 | ||
|
|
285e52cc7c | ||
|
|
d52c969f94 | ||
|
|
63c3e6f58c | ||
|
|
0ab76bb8bc | ||
|
|
7fc577c31d | ||
|
|
3a43110f06 | ||
|
|
87d79d4d99 | ||
|
|
83581c3a0f | ||
|
|
ba90f55075 | ||
|
|
3313dcb1ce | ||
|
|
92d56fab47 | ||
|
|
1208f92d9c | ||
|
|
8444551373 | ||
|
|
73ebd7e560 | ||
|
|
0a96f86f74 | ||
|
|
36176befb0 | ||
|
|
de08da278d | ||
|
|
4c2eb17ccd | ||
|
|
8fb44db92b | ||
|
|
4105c3017c | ||
|
|
7f2f4eef48 | ||
|
|
666c3cb1c7 | ||
|
|
886134c1f3 | ||
|
|
ba61a6c5fb | ||
|
|
3f14df374f | ||
|
|
e6755d1e7c | ||
|
|
c4f59e731d | ||
|
|
805ed344c0 | ||
|
|
a5959d9be2 | ||
|
|
0375dccf64 | ||
|
|
3c4bb5358e | ||
|
|
e317d2db9d | ||
|
|
3daecfa8e4 | ||
|
|
cf93362368 | ||
|
|
23d4eda2a5 | ||
|
|
718ae13ae1 | ||
|
|
cddbe9fbf1 | ||
|
|
9c8173dbfd | ||
|
|
77ff37a853 | ||
|
|
40341674bd | ||
|
|
62ebdce5a9 | ||
|
|
58de4e0c26 | ||
|
|
dbd6c62b70 | ||
|
|
887c6753f8 | ||
|
|
bd35896892 | ||
|
|
9286e62449 | ||
|
|
621d1a5167 | ||
|
|
83714fbac2 | ||
|
|
413921a287 | ||
|
|
7ee36829ac | ||
|
|
bfb46b37d3 | ||
|
|
09d2bdbb21 | ||
|
|
8733d09a9c | ||
|
|
e524cce222 | ||
|
|
be6b811c4e | ||
|
|
bdb9a280bc | ||
|
|
73ca4eb599 | ||
|
|
569ccbadec | ||
|
|
ed1b584c42 | ||
|
|
2f2e946907 | ||
|
|
d392f70cc6 | ||
|
|
db164cefd3 | ||
|
|
4d613d3ba7 | ||
|
|
1f26841e23 | ||
|
|
9370cb033c | ||
|
|
ab0ddb593f | ||
|
|
f67503d9fd | ||
|
|
ce729b0721 | ||
|
|
8156cdc56e | ||
|
|
8cc8e61474 | ||
|
|
29b0ffe5e9 | ||
|
|
a772a0d7d7 | ||
|
|
6f0096c87b | ||
|
|
da41ed22f9 | ||
|
|
d6fa8596d2 | ||
|
|
a9b4fe768d | ||
|
|
88e53e177d | ||
|
|
e168fd03ca | ||
|
|
95a23eb682 | ||
|
|
f5ad363143 | ||
|
|
f290faf4ba | ||
|
|
725088a18b | ||
|
|
bf672ec340 | ||
|
|
0e4f9c9a66 | ||
|
|
5fdb75b541 | ||
|
|
24d4a1045a | ||
|
|
514f0650b2 | ||
|
|
20d34c8b14 | ||
|
|
6f45eb7959 | ||
|
|
49b98fa111 | ||
|
|
6048630a11 | ||
|
|
46de4411a7 | ||
|
|
883f251e7d | ||
|
|
284cda087e | ||
|
|
b2f9c182f3 | ||
|
|
558098d322 | ||
|
|
6571e079b9 | ||
|
|
49ca23c034 | ||
|
|
709bd9c363 | ||
|
|
8cecf2e02d | ||
|
|
d59c759cdd | ||
|
|
7b5d5fcd58 | ||
|
|
d01f712376 | ||
|
|
ac75d35927 | ||
|
|
605d7f26e7 | ||
|
|
b24ca75914 | ||
|
|
2b75741e5a | ||
|
|
7ff8c2b224 | ||
|
|
db31adc208 | ||
|
|
805f6a7683 | ||
|
|
d92f323e6d | ||
|
|
cf2dbf55b8 | ||
|
|
8d4c724c2d | ||
|
|
9cb2770da4 | ||
|
|
6a23491fa9 | ||
|
|
294b9742be | ||
|
|
a9b1f15f92 | ||
|
|
aa7c7cdf93 | ||
|
|
28139ab90d | ||
|
|
d0792b49fa | ||
|
|
5548aa5c79 | ||
|
|
16440ff055 | ||
|
|
7850d6de45 | ||
|
|
74b4fb89bb | ||
|
|
22ccf35fa1 | ||
|
|
7ad1fe24bd | ||
|
|
450ba978c1 | ||
|
|
3d6946417d | ||
|
|
31cf63b374 | ||
|
|
5c853c4a2c | ||
|
|
ad922cd7a1 | ||
|
|
49bafdc4cd | ||
|
|
989b2491b9 | ||
|
|
ca2ce3a034 | ||
|
|
dfe9dccab8 | ||
|
|
d456c3909d | ||
|
|
29ceef6d93 | ||
|
|
8cff440800 | ||
|
|
e5f6ae767d | ||
|
|
cd44179305 | ||
|
|
c3c5b354b8 | ||
|
|
95cf195dbd | ||
|
|
a80afd67ab | ||
|
|
4bc4d273ac | ||
|
|
4911c77134 | ||
|
|
c1b9a76a54 | ||
|
|
c31e25af72 | ||
|
|
c8295d36cc | ||
|
|
b12c29479e | ||
|
|
cd47829f3d | ||
|
|
4d4ef4e0b3 | ||
|
|
882ef2ccd8 | ||
|
|
d6cd76c3c1 | ||
|
|
bd0be2cdc7 | ||
|
|
a8d7ebd987 | ||
|
|
00f61196a4 | ||
|
|
c21d6706b6 | ||
|
|
c3c5d91c47 | ||
|
|
7fa4cd1214 | ||
|
|
f353d9fbc0 | ||
|
|
09018855ce | ||
|
|
719954b02f | ||
|
|
67bc3fabe4 | ||
|
|
e724a346c7 | ||
|
|
87b4545b44 | ||
|
|
58a7844129 | ||
|
|
4353f7b9f9 | ||
|
|
8f8693e13e | ||
|
|
363a6563c7 | ||
|
|
59d6af73fa | ||
|
|
cd7f67018e | ||
|
|
b7e8770c4f | ||
|
|
ad4cc5d6df | ||
|
|
ca14ed68f7 | ||
|
|
71514cb380 | ||
|
|
8212f1bd45 | ||
|
|
dca3bbdea3 | ||
|
|
8ed7dfef6f | ||
|
|
631f5be02f | ||
|
|
4f4ea2a402 | ||
|
|
5a5bffebd1 | ||
|
|
8749bc0844 | ||
|
|
f3d0c63db2 | ||
|
|
93a846db31 | ||
|
|
686c25d50f | ||
|
|
ef6555f084 | ||
|
|
3c6652c101 | ||
|
|
43af1684c1 | ||
|
|
ed549155b3 | ||
|
|
39ae91c81c | ||
|
|
b6acb3cd8c | ||
|
|
a467a8a094 | ||
|
|
78227c3c06 | ||
|
|
e4e802d1f8 | ||
|
|
b24a60ba9f | ||
|
|
461b600068 |
91 changed files with 4360 additions and 1640 deletions
172
CHANGELOG.md
172
CHANGELOG.md
|
|
@ -1,6 +1,178 @@
|
||||||
Change log
|
Change log
|
||||||
==========
|
==========
|
||||||
|
|
||||||
|
1.6.1 (2016-02-23)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
|
||||||
|
- Fixed a bug where recreating a container multiple times would cause the
|
||||||
|
new container to be started without the previous volumes.
|
||||||
|
|
||||||
|
- Fixed a bug where Compose would set the value of unset environment variables
|
||||||
|
to an empty string, instead of a key without a value.
|
||||||
|
|
||||||
|
- Provide a better error message when Compose requires a more recent version
|
||||||
|
of the Docker API.
|
||||||
|
|
||||||
|
- Add a missing config field `network.aliases` which allows setting a network
|
||||||
|
scoped alias for a service.
|
||||||
|
|
||||||
|
- Fixed a bug where `run` would not start services listed in `depends_on`.
|
||||||
|
|
||||||
|
- Fixed a bug where `networks` and `network_mode` where not merged when using
|
||||||
|
extends or multiple Compose files.
|
||||||
|
|
||||||
|
- Fixed a bug with service aliases where the short container id alias was
|
||||||
|
only contained 10 characters, instead of the 12 characters used in previous
|
||||||
|
versions.
|
||||||
|
|
||||||
|
- Added a missing log message when creating a new named volume.
|
||||||
|
|
||||||
|
- Fixed a bug where `build.args` was not merged when using `extends` or
|
||||||
|
multiple Compose files.
|
||||||
|
|
||||||
|
- Fixed some bugs with config validation when null values or incorrect types
|
||||||
|
were used instead of a mapping.
|
||||||
|
|
||||||
|
- Fixed a bug where a `build` section without a `context` would show a stack
|
||||||
|
trace instead of a helpful validation message.
|
||||||
|
|
||||||
|
- Improved compatibility with swarm by only setting a container affinity to
|
||||||
|
the previous instance of a services' container when the service uses an
|
||||||
|
anonymous container volume. Previously the affinity was always set on all
|
||||||
|
containers.
|
||||||
|
|
||||||
|
- Fixed the validation of some `driver_opts` would cause an error if a number
|
||||||
|
was used instead of a string.
|
||||||
|
|
||||||
|
- Some improvements to the `run.sh` script used by the Compose container install
|
||||||
|
option.
|
||||||
|
|
||||||
|
- Fixed a bug with `up --abort-on-container-exit` where Compose would exit,
|
||||||
|
but would not stop other containers.
|
||||||
|
|
||||||
|
- Corrected the warning message that is printed when a boolean value is used
|
||||||
|
as a value in a mapping.
|
||||||
|
|
||||||
|
|
||||||
|
1.6.0 (2016-01-15)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Major Features:
|
||||||
|
|
||||||
|
- Compose 1.6 introduces a new format for `docker-compose.yml` which lets
|
||||||
|
you define networks and volumes in the Compose file as well as services. It
|
||||||
|
also makes a few changes to the structure of some configuration options.
|
||||||
|
|
||||||
|
You don't have to use it - your existing Compose files will run on Compose
|
||||||
|
1.6 exactly as they do today.
|
||||||
|
|
||||||
|
Check the upgrade guide for full details:
|
||||||
|
https://docs.docker.com/compose/compose-file#upgrading
|
||||||
|
|
||||||
|
- Support for networking has exited experimental status and is the recommended
|
||||||
|
way to enable communication between containers.
|
||||||
|
|
||||||
|
If you use the new file format, your app will use networking. If you aren't
|
||||||
|
ready yet, just leave your Compose file as it is and it'll continue to work
|
||||||
|
just the same.
|
||||||
|
|
||||||
|
By default, you don't have to configure any networks. In fact, using
|
||||||
|
networking with Compose involves even less configuration than using links.
|
||||||
|
Consult the networking guide for how to use it:
|
||||||
|
https://docs.docker.com/compose/networking
|
||||||
|
|
||||||
|
The experimental flags `--x-networking` and `--x-network-driver`, introduced
|
||||||
|
in Compose 1.5, have been removed.
|
||||||
|
|
||||||
|
- You can now pass arguments to a build if you're using the new file format:
|
||||||
|
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
buildno: 1
|
||||||
|
|
||||||
|
- You can now specify both a `build` and an `image` key if you're using the
|
||||||
|
new file format. `docker-compose build` will build the image and tag it with
|
||||||
|
the name you've specified, while `docker-compose pull` will attempt to pull
|
||||||
|
it.
|
||||||
|
|
||||||
|
- There's a new `events` command for monitoring container events from
|
||||||
|
the application, much like `docker events`. This is a good primitive for
|
||||||
|
building tools on top of Compose for performing actions when particular
|
||||||
|
things happen, such as containers starting and stopping.
|
||||||
|
|
||||||
|
- There's a new `depends_on` option for specifying dependencies between
|
||||||
|
services. This enforces the order of startup, and ensures that when you run
|
||||||
|
`docker-compose up SERVICE` on a service with dependencies, those are started
|
||||||
|
as well.
|
||||||
|
|
||||||
|
New Features:
|
||||||
|
|
||||||
|
- Added a new command `config` which validates and prints the Compose
|
||||||
|
configuration after interpolating variables, resolving relative paths, and
|
||||||
|
merging multiple files and `extends`.
|
||||||
|
|
||||||
|
- Added a new command `create` for creating containers without starting them.
|
||||||
|
|
||||||
|
- Added a new command `down` to stop and remove all the resources created by
|
||||||
|
`up` in a single command.
|
||||||
|
|
||||||
|
- Added support for the `cpu_quota` configuration option.
|
||||||
|
|
||||||
|
- Added support for the `stop_signal` configuration option.
|
||||||
|
|
||||||
|
- Commands `start`, `restart`, `pause`, and `unpause` now exit with an
|
||||||
|
error status code if no containers were modified.
|
||||||
|
|
||||||
|
- Added a new `--abort-on-container-exit` flag to `up` which causes `up` to
|
||||||
|
stop all container and exit once the first container exits.
|
||||||
|
|
||||||
|
- Removed support for `FIG_FILE`, `FIG_PROJECT_NAME`, and no longer reads
|
||||||
|
`fig.yml` as a default Compose file location.
|
||||||
|
|
||||||
|
- Removed the `migrate-to-labels` command.
|
||||||
|
|
||||||
|
- Removed the `--allow-insecure-ssl` flag.
|
||||||
|
|
||||||
|
|
||||||
|
Bug Fixes:
|
||||||
|
|
||||||
|
- Fixed a validation bug that prevented the use of a range of ports in
|
||||||
|
the `expose` field.
|
||||||
|
|
||||||
|
- Fixed a validation bug that prevented the use of arrays in the `entrypoint`
|
||||||
|
field if they contained duplicate entries.
|
||||||
|
|
||||||
|
- Fixed a bug that caused `ulimits` to be ignored when used with `extends`.
|
||||||
|
|
||||||
|
- Fixed a bug that prevented ipv6 addresses in `extra_hosts`.
|
||||||
|
|
||||||
|
- Fixed a bug that caused `extends` to be ignored when included from
|
||||||
|
multiple Compose files.
|
||||||
|
|
||||||
|
- Fixed an incorrect warning when a container volume was defined in
|
||||||
|
the Compose file.
|
||||||
|
|
||||||
|
- Fixed a bug that prevented the force shutdown behaviour of `up` and
|
||||||
|
`logs`.
|
||||||
|
|
||||||
|
- Fixed a bug that caused `None` to be printed as the network driver name
|
||||||
|
when the default network driver was used.
|
||||||
|
|
||||||
|
- Fixed a bug where using the string form of `dns` or `dns_search` would
|
||||||
|
cause an error.
|
||||||
|
|
||||||
|
- Fixed a bug where a container would be reported as "Up" when it was
|
||||||
|
in the restarting state.
|
||||||
|
|
||||||
|
- Fixed a confusing error message when DOCKER_CERT_PATH was not set properly.
|
||||||
|
|
||||||
|
- Fixed a bug where attaching to a container would fail if it was using a
|
||||||
|
non-standard logging driver (or none at all).
|
||||||
|
|
||||||
|
|
||||||
1.5.2 (2015-12-03)
|
1.5.2 (2015-12-03)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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](docs/index.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](docs/index.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.
|
||||||
|
|
||||||
|
|
@ -34,7 +34,7 @@ A `docker-compose.yml` looks like this:
|
||||||
image: redis
|
image: redis
|
||||||
|
|
||||||
For more information about the Compose file, see the
|
For more information about the Compose file, see the
|
||||||
[Compose file reference](docs/compose-file.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:
|
||||||
|
|
||||||
|
|
|
||||||
40
SWARM.md
40
SWARM.md
|
|
@ -1,39 +1 @@
|
||||||
Docker Compose/Swarm integration
|
This file has moved to: https://docs.docker.com/compose/swarm/
|
||||||
================================
|
|
||||||
|
|
||||||
Eventually, Compose and Swarm 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.
|
|
||||||
|
|
||||||
However, integration is currently incomplete: Compose can create containers on a Swarm cluster, but the majority of Compose apps won’t work out of the box unless all containers are scheduled on one host, because links between containers do not work across hosts.
|
|
||||||
|
|
||||||
Docker networking is [getting overhauled](https://github.com/docker/libnetwork) in such a way that it’ll fit the multi-host model much better. For now, linked containers are automatically scheduled on the same host.
|
|
||||||
|
|
||||||
Building
|
|
||||||
--------
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
environment:
|
|
||||||
# Schedule containers on a node that has the 'storage' label set to 'ssd'
|
|
||||||
- "constraint:storage==ssd"
|
|
||||||
|
|
||||||
# Schedule containers where the 'redis' image is already pulled
|
|
||||||
- "affinity:image==redis"
|
|
||||||
|
|
||||||
For the full set of available filters and expressions, see the [Swarm documentation](https://docs.docker.com/swarm/scheduler/filter/).
|
|
||||||
|
|
|
||||||
|
|
@ -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.6.0dev'
|
__version__ = '1.6.1'
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ def get_project_name(working_dir, project_name=None):
|
||||||
return re.sub(r'[^a-z0-9]', '', name.lower())
|
return re.sub(r'[^a-z0-9]', '', name.lower())
|
||||||
|
|
||||||
project_name = project_name or os.environ.get('COMPOSE_PROJECT_NAME')
|
project_name = project_name or os.environ.get('COMPOSE_PROJECT_NAME')
|
||||||
if project_name is not None:
|
if project_name:
|
||||||
return normalize_name(project_name)
|
return normalize_name(project_name)
|
||||||
|
|
||||||
project = os.path.basename(os.path.abspath(working_dir))
|
project = os.path.basename(os.path.abspath(working_dir))
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,11 @@ import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from docker import Client
|
from docker import Client
|
||||||
|
from docker.errors import TLSParameterError
|
||||||
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
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -20,8 +22,16 @@ def docker_client(version=None):
|
||||||
if 'DOCKER_CLIENT_TIMEOUT' in os.environ:
|
if 'DOCKER_CLIENT_TIMEOUT' in os.environ:
|
||||||
log.warn('The DOCKER_CLIENT_TIMEOUT environment variable is deprecated. Please use COMPOSE_HTTP_TIMEOUT instead.')
|
log.warn('The DOCKER_CLIENT_TIMEOUT environment variable is deprecated. Please use COMPOSE_HTTP_TIMEOUT instead.')
|
||||||
|
|
||||||
kwargs = kwargs_from_env(assert_hostname=False)
|
try:
|
||||||
|
kwargs = kwargs_from_env(assert_hostname=False)
|
||||||
|
except TLSParameterError:
|
||||||
|
raise UserError(
|
||||||
|
'TLS configuration is invalid - make sure your DOCKER_TLS_VERIFY and DOCKER_CERT_PATH are set correctly.\n'
|
||||||
|
'You might need to run `eval "$(docker-machine env default)"`')
|
||||||
|
|
||||||
if version:
|
if version:
|
||||||
kwargs['version'] = version
|
kwargs['version'] = version
|
||||||
|
|
||||||
kwargs['timeout'] = HTTP_TIMEOUT
|
kwargs['timeout'] = HTTP_TIMEOUT
|
||||||
|
|
||||||
return Client(**kwargs)
|
return Client(**kwargs)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from __future__ import absolute_import
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
@ -18,6 +19,7 @@ from ..config import config
|
||||||
from ..config import ConfigurationError
|
from ..config import ConfigurationError
|
||||||
from ..config import parse_environment
|
from ..config import parse_environment
|
||||||
from ..config.serialize import serialize_config
|
from ..config.serialize import serialize_config
|
||||||
|
from ..const import API_VERSION_TO_ENGINE_VERSION
|
||||||
from ..const import DEFAULT_TIMEOUT
|
from ..const import DEFAULT_TIMEOUT
|
||||||
from ..const import HTTP_TIMEOUT
|
from ..const import HTTP_TIMEOUT
|
||||||
from ..const import IS_WINDOWS_PLATFORM
|
from ..const import IS_WINDOWS_PLATFORM
|
||||||
|
|
@ -41,7 +43,7 @@ from .utils import yesno
|
||||||
|
|
||||||
|
|
||||||
if not IS_WINDOWS_PLATFORM:
|
if not IS_WINDOWS_PLATFORM:
|
||||||
import dockerpty
|
from dockerpty.pty import PseudoTerminal, RunOperation
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
console_handler = logging.StreamHandler(sys.stderr)
|
console_handler = logging.StreamHandler(sys.stderr)
|
||||||
|
|
@ -53,7 +55,7 @@ def main():
|
||||||
command = TopLevelCommand()
|
command = TopLevelCommand()
|
||||||
command.sys_dispatch()
|
command.sys_dispatch()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
log.error("\nAborting.")
|
log.error("Aborting.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except (UserError, NoSuchService, ConfigurationError) as e:
|
except (UserError, NoSuchService, ConfigurationError) as e:
|
||||||
log.error(e.msg)
|
log.error(e.msg)
|
||||||
|
|
@ -63,7 +65,7 @@ def main():
|
||||||
log.error("No such command: %s\n\n%s", e.command, commands)
|
log.error("No such command: %s\n\n%s", e.command, commands)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except APIError as e:
|
except APIError as e:
|
||||||
log.error(e.explanation)
|
log_api_error(e)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except BuildError as e:
|
except BuildError as e:
|
||||||
log.error("Service '%s' failed to build: %s" % (e.service.name, e.reason))
|
log.error("Service '%s' failed to build: %s" % (e.service.name, e.reason))
|
||||||
|
|
@ -83,6 +85,22 @@ def main():
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def log_api_error(e):
|
||||||
|
if 'client is newer than server' in e.explanation:
|
||||||
|
# we need JSON formatted errors. In the meantime...
|
||||||
|
# TODO: fix this by refactoring project dispatch
|
||||||
|
# http://github.com/docker/compose/pull/2832#commitcomment-15923800
|
||||||
|
client_version = e.explanation.split('client API version: ')[1].split(',')[0]
|
||||||
|
log.error(
|
||||||
|
"The engine version is lesser than the minimum required by "
|
||||||
|
"compose. Your current project requires a Docker Engine of "
|
||||||
|
"version {version} or superior.".format(
|
||||||
|
version=API_VERSION_TO_ENGINE_VERSION[client_version]
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
log.error(e.explanation)
|
||||||
|
|
||||||
|
|
||||||
def setup_logging():
|
def setup_logging():
|
||||||
root_logger = logging.getLogger()
|
root_logger = logging.getLogger()
|
||||||
root_logger.addHandler(console_handler)
|
root_logger.addHandler(console_handler)
|
||||||
|
|
@ -629,18 +647,24 @@ class TopLevelCommand(DocoptCommand):
|
||||||
if detached and cascade_stop:
|
if detached and cascade_stop:
|
||||||
raise UserError("--abort-on-container-exit and -d cannot be combined.")
|
raise UserError("--abort-on-container-exit and -d cannot be combined.")
|
||||||
|
|
||||||
to_attach = project.up(
|
with up_shutdown_context(project, service_names, timeout, detached):
|
||||||
service_names=service_names,
|
to_attach = project.up(
|
||||||
start_deps=start_deps,
|
service_names=service_names,
|
||||||
strategy=convergence_strategy_from_opts(options),
|
start_deps=start_deps,
|
||||||
do_build=not options['--no-build'],
|
strategy=convergence_strategy_from_opts(options),
|
||||||
timeout=timeout,
|
do_build=not options['--no-build'],
|
||||||
detached=detached
|
timeout=timeout,
|
||||||
)
|
detached=detached)
|
||||||
|
|
||||||
if not detached:
|
if detached:
|
||||||
|
return
|
||||||
log_printer = build_log_printer(to_attach, service_names, monochrome, cascade_stop)
|
log_printer = build_log_printer(to_attach, service_names, monochrome, cascade_stop)
|
||||||
attach_to_logs(project, log_printer, service_names, timeout)
|
print("Attaching to", list_containers(log_printer.containers))
|
||||||
|
log_printer.run()
|
||||||
|
|
||||||
|
if cascade_stop:
|
||||||
|
print("Aborting on container exit...")
|
||||||
|
project.stop(service_names=service_names, timeout=timeout)
|
||||||
|
|
||||||
def version(self, project, options):
|
def version(self, project, options):
|
||||||
"""
|
"""
|
||||||
|
|
@ -683,14 +707,14 @@ def image_type_from_opt(flag, value):
|
||||||
|
|
||||||
def run_one_off_container(container_options, project, service, options):
|
def run_one_off_container(container_options, project, service, options):
|
||||||
if not options['--no-deps']:
|
if not options['--no-deps']:
|
||||||
deps = service.get_linked_service_names()
|
deps = service.get_dependency_names()
|
||||||
if deps:
|
if deps:
|
||||||
project.up(
|
project.up(
|
||||||
service_names=deps,
|
service_names=deps,
|
||||||
start_deps=True,
|
start_deps=True,
|
||||||
strategy=ConvergenceStrategy.never)
|
strategy=ConvergenceStrategy.never)
|
||||||
|
|
||||||
project.initialize_networks()
|
project.initialize()
|
||||||
|
|
||||||
container = service.create_container(
|
container = service.create_container(
|
||||||
quiet=True,
|
quiet=True,
|
||||||
|
|
@ -709,8 +733,16 @@ def run_one_off_container(container_options, project, service, options):
|
||||||
signals.set_signal_handler_to_shutdown()
|
signals.set_signal_handler_to_shutdown()
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
dockerpty.start(project.client, container.id, interactive=not options['-T'])
|
operation = RunOperation(
|
||||||
service.connect_container_to_networks(container)
|
project.client,
|
||||||
|
container.id,
|
||||||
|
interactive=not options['-T'],
|
||||||
|
logs=False,
|
||||||
|
)
|
||||||
|
pty = PseudoTerminal(project.client, operation)
|
||||||
|
sockets = pty.sockets()
|
||||||
|
service.start_container(container)
|
||||||
|
pty.start(sockets)
|
||||||
exit_code = container.wait()
|
exit_code = container.wait()
|
||||||
except signals.ShutdownException:
|
except signals.ShutdownException:
|
||||||
project.client.stop(container.id)
|
project.client.stop(container.id)
|
||||||
|
|
@ -733,13 +765,16 @@ def build_log_printer(containers, service_names, monochrome, cascade_stop):
|
||||||
return LogPrinter(containers, monochrome=monochrome, cascade_stop=cascade_stop)
|
return LogPrinter(containers, monochrome=monochrome, cascade_stop=cascade_stop)
|
||||||
|
|
||||||
|
|
||||||
def attach_to_logs(project, log_printer, service_names, timeout):
|
@contextlib.contextmanager
|
||||||
print("Attaching to", list_containers(log_printer.containers))
|
def up_shutdown_context(project, service_names, timeout, detached):
|
||||||
signals.set_signal_handler_to_shutdown()
|
if detached:
|
||||||
|
yield
|
||||||
|
return
|
||||||
|
|
||||||
|
signals.set_signal_handler_to_shutdown()
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
log_printer.run()
|
yield
|
||||||
except signals.ShutdownException:
|
except signals.ShutdownException:
|
||||||
print("Gracefully stopping... (press Ctrl+C again to force)")
|
print("Gracefully stopping... (press Ctrl+C again to force)")
|
||||||
project.stop(service_names=service_names, timeout=timeout)
|
project.stop(service_names=service_names, timeout=timeout)
|
||||||
|
|
|
||||||
|
|
@ -14,22 +14,31 @@ import six
|
||||||
import yaml
|
import yaml
|
||||||
from cached_property import cached_property
|
from cached_property import cached_property
|
||||||
|
|
||||||
from ..const import COMPOSEFILE_VERSIONS
|
from ..const import COMPOSEFILE_V1 as V1
|
||||||
|
from ..const import COMPOSEFILE_V2_0 as V2_0
|
||||||
|
from ..utils import build_string_dict
|
||||||
from .errors import CircularReference
|
from .errors import CircularReference
|
||||||
from .errors import ComposeFileNotFound
|
from .errors import ComposeFileNotFound
|
||||||
from .errors import ConfigurationError
|
from .errors import ConfigurationError
|
||||||
|
from .errors import VERSION_EXPLANATION
|
||||||
from .interpolation import interpolate_environment_variables
|
from .interpolation import interpolate_environment_variables
|
||||||
from .sort_services import get_service_name_from_net
|
from .sort_services import get_container_name_from_network_mode
|
||||||
|
from .sort_services import get_service_name_from_network_mode
|
||||||
from .sort_services import sort_service_dicts
|
from .sort_services import sort_service_dicts
|
||||||
from .types import parse_extra_hosts
|
from .types import parse_extra_hosts
|
||||||
from .types import parse_restart_spec
|
from .types import parse_restart_spec
|
||||||
|
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 validate_against_fields_schema
|
from .validation import validate_against_fields_schema
|
||||||
from .validation import validate_against_service_schema
|
from .validation import validate_against_service_schema
|
||||||
|
from .validation import validate_config_section
|
||||||
|
from .validation import validate_depends_on
|
||||||
from .validation import validate_extends_file_path
|
from .validation import validate_extends_file_path
|
||||||
|
from .validation import validate_network_mode
|
||||||
from .validation import validate_top_level_object
|
from .validation import validate_top_level_object
|
||||||
from .validation import validate_top_level_service_objects
|
from .validation import validate_ulimits
|
||||||
|
|
||||||
|
|
||||||
DOCKER_CONFIG_KEYS = [
|
DOCKER_CONFIG_KEYS = [
|
||||||
|
|
@ -78,9 +87,8 @@ ALLOWED_KEYS = DOCKER_CONFIG_KEYS + [
|
||||||
'build',
|
'build',
|
||||||
'container_name',
|
'container_name',
|
||||||
'dockerfile',
|
'dockerfile',
|
||||||
'expose',
|
|
||||||
'external_links',
|
|
||||||
'logging',
|
'logging',
|
||||||
|
'network_mode',
|
||||||
]
|
]
|
||||||
|
|
||||||
DOCKER_VALID_URL_PREFIXES = (
|
DOCKER_VALID_URL_PREFIXES = (
|
||||||
|
|
@ -98,6 +106,7 @@ SUPPORTED_FILENAMES = [
|
||||||
|
|
||||||
DEFAULT_OVERRIDE_FILENAME = 'docker-compose.override.yml'
|
DEFAULT_OVERRIDE_FILENAME = 'docker-compose.override.yml'
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -124,24 +133,48 @@ class ConfigFile(namedtuple('_ConfigFile', 'filename config')):
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def version(self):
|
def version(self):
|
||||||
if self.config is None:
|
if 'version' not in self.config:
|
||||||
return 1
|
return V1
|
||||||
version = self.config.get('version', 1)
|
|
||||||
|
version = self.config['version']
|
||||||
|
|
||||||
if isinstance(version, dict):
|
if isinstance(version, dict):
|
||||||
log.warn("Unexpected type for field 'version', in file {} assuming "
|
log.warn('Unexpected type for "version" key in "{}". Assuming '
|
||||||
"version is the name of a service, and defaulting to "
|
'"version" is the name of a service, and defaulting to '
|
||||||
"Compose file version 1".format(self.filename))
|
'Compose file version 1.'.format(self.filename))
|
||||||
return 1
|
return V1
|
||||||
|
|
||||||
|
if not isinstance(version, six.string_types):
|
||||||
|
raise ConfigurationError(
|
||||||
|
'Version in "{}" is invalid - it should be a string.'
|
||||||
|
.format(self.filename))
|
||||||
|
|
||||||
|
if version == '1':
|
||||||
|
raise ConfigurationError(
|
||||||
|
'Version in "{}" is invalid. {}'
|
||||||
|
.format(self.filename, VERSION_EXPLANATION))
|
||||||
|
|
||||||
|
if version == '2':
|
||||||
|
version = V2_0
|
||||||
|
|
||||||
|
if version != V2_0:
|
||||||
|
raise ConfigurationError(
|
||||||
|
'Version in "{}" is unsupported. {}'
|
||||||
|
.format(self.filename, VERSION_EXPLANATION))
|
||||||
|
|
||||||
return version
|
return version
|
||||||
|
|
||||||
|
def get_service(self, name):
|
||||||
|
return self.get_service_dicts()[name]
|
||||||
|
|
||||||
def get_service_dicts(self):
|
def get_service_dicts(self):
|
||||||
return self.config if self.version == 1 else self.config.get('services', {})
|
return self.config if self.version == V1 else self.config.get('services', {})
|
||||||
|
|
||||||
def get_volumes(self):
|
def get_volumes(self):
|
||||||
return {} if self.version == 1 else self.config.get('volumes', {})
|
return {} if self.version == V1 else self.config.get('volumes', {})
|
||||||
|
|
||||||
def get_networks(self):
|
def get_networks(self):
|
||||||
return {} if self.version == 1 else self.config.get('networks', {})
|
return {} if self.version == V1 else self.config.get('networks', {})
|
||||||
|
|
||||||
|
|
||||||
class Config(namedtuple('_Config', 'version services volumes networks')):
|
class Config(namedtuple('_Config', 'version services volumes networks')):
|
||||||
|
|
@ -188,10 +221,10 @@ def find(base_dir, filenames):
|
||||||
[ConfigFile.from_filename(f) for f in filenames])
|
[ConfigFile.from_filename(f) for f in filenames])
|
||||||
|
|
||||||
|
|
||||||
def validate_config_version(config_details):
|
def validate_config_version(config_files):
|
||||||
main_file = config_details.config_files[0]
|
main_file = config_files[0]
|
||||||
validate_top_level_object(main_file)
|
validate_top_level_object(main_file)
|
||||||
for next_file in config_details.config_files[1:]:
|
for next_file in config_files[1:]:
|
||||||
validate_top_level_object(next_file)
|
validate_top_level_object(next_file)
|
||||||
|
|
||||||
if main_file.version != next_file.version:
|
if main_file.version != next_file.version:
|
||||||
|
|
@ -203,10 +236,6 @@ def validate_config_version(config_details):
|
||||||
next_file.filename,
|
next_file.filename,
|
||||||
next_file.version))
|
next_file.version))
|
||||||
|
|
||||||
if main_file.version not in COMPOSEFILE_VERSIONS:
|
|
||||||
raise ConfigurationError(
|
|
||||||
'Invalid Compose file version: {0}'.format(main_file.version))
|
|
||||||
|
|
||||||
|
|
||||||
def get_default_config_files(base_dir):
|
def get_default_config_files(base_dir):
|
||||||
(candidates, path) = find_candidates_in_parent_dirs(SUPPORTED_FILENAMES, base_dir)
|
(candidates, path) = find_candidates_in_parent_dirs(SUPPORTED_FILENAMES, base_dir)
|
||||||
|
|
@ -254,7 +283,7 @@ def load(config_details):
|
||||||
|
|
||||||
Return a fully interpolated, extended and validated configuration.
|
Return a fully interpolated, extended and validated configuration.
|
||||||
"""
|
"""
|
||||||
validate_config_version(config_details)
|
validate_config_version(config_details.config_files)
|
||||||
|
|
||||||
processed_files = [
|
processed_files = [
|
||||||
process_config_file(config_file)
|
process_config_file(config_file)
|
||||||
|
|
@ -263,13 +292,21 @@ def load(config_details):
|
||||||
config_details = config_details._replace(config_files=processed_files)
|
config_details = config_details._replace(config_files=processed_files)
|
||||||
|
|
||||||
main_file = config_details.config_files[0]
|
main_file = config_details.config_files[0]
|
||||||
volumes = load_mapping(config_details.config_files, 'get_volumes', 'Volume')
|
volumes = load_mapping(
|
||||||
networks = load_mapping(config_details.config_files, 'get_networks', 'Network')
|
config_details.config_files, 'get_volumes', 'Volume'
|
||||||
|
)
|
||||||
|
networks = load_mapping(
|
||||||
|
config_details.config_files, 'get_networks', 'Network'
|
||||||
|
)
|
||||||
service_dicts = load_services(
|
service_dicts = load_services(
|
||||||
config_details.working_dir,
|
config_details.working_dir,
|
||||||
main_file.filename,
|
main_file,
|
||||||
[file.get_service_dicts() for file in config_details.config_files],
|
[file.get_service_dicts() for file in config_details.config_files])
|
||||||
main_file.version)
|
|
||||||
|
if main_file.version != V1:
|
||||||
|
for service_dict in service_dicts:
|
||||||
|
match_named_volumes(service_dict, volumes)
|
||||||
|
|
||||||
return Config(main_file.version, service_dicts, volumes, networks)
|
return Config(main_file.version, service_dicts, volumes, networks)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -300,24 +337,30 @@ def load_mapping(config_files, get_func, entity_type):
|
||||||
|
|
||||||
mapping[name] = config
|
mapping[name] = config
|
||||||
|
|
||||||
|
if 'driver_opts' in config:
|
||||||
|
config['driver_opts'] = build_string_dict(
|
||||||
|
config['driver_opts']
|
||||||
|
)
|
||||||
|
|
||||||
return mapping
|
return mapping
|
||||||
|
|
||||||
|
|
||||||
def load_services(working_dir, filename, service_configs, version):
|
def load_services(working_dir, config_file, service_configs):
|
||||||
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(
|
||||||
working_dir,
|
working_dir,
|
||||||
filename,
|
config_file.filename,
|
||||||
service_name,
|
service_name,
|
||||||
service_dict)
|
service_dict)
|
||||||
resolver = ServiceExtendsResolver(service_config, version)
|
resolver = ServiceExtendsResolver(service_config, config_file)
|
||||||
service_dict = process_service(resolver.run())
|
service_dict = process_service(resolver.run())
|
||||||
|
|
||||||
validate_service(service_dict, service_config.name, version)
|
service_config = service_config._replace(config=service_dict)
|
||||||
|
validate_service(service_config, service_names, config_file.version)
|
||||||
service_dict = finalize_service(
|
service_dict = finalize_service(
|
||||||
service_config._replace(config=service_dict),
|
service_config,
|
||||||
service_names,
|
service_names,
|
||||||
version)
|
config_file.version)
|
||||||
return service_dict
|
return service_dict
|
||||||
|
|
||||||
def build_services(service_config):
|
def build_services(service_config):
|
||||||
|
|
@ -333,7 +376,7 @@ def load_services(working_dir, filename, service_configs, version):
|
||||||
name: merge_service_dicts_from_files(
|
name: merge_service_dicts_from_files(
|
||||||
base.get(name, {}),
|
base.get(name, {}),
|
||||||
override.get(name, {}),
|
override.get(name, {}),
|
||||||
version)
|
config_file.version)
|
||||||
for name in all_service_names
|
for name in all_service_names
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -344,27 +387,36 @@ def load_services(working_dir, filename, service_configs, version):
|
||||||
return build_services(service_config)
|
return build_services(service_config)
|
||||||
|
|
||||||
|
|
||||||
|
def interpolate_config_section(filename, config, section):
|
||||||
|
validate_config_section(filename, config, section)
|
||||||
|
return interpolate_environment_variables(config, section)
|
||||||
|
|
||||||
|
|
||||||
def process_config_file(config_file, service_name=None):
|
def process_config_file(config_file, service_name=None):
|
||||||
service_dicts = config_file.get_service_dicts()
|
services = interpolate_config_section(
|
||||||
validate_top_level_service_objects(config_file.filename, service_dicts)
|
config_file.filename,
|
||||||
|
config_file.get_service_dicts(),
|
||||||
|
'service')
|
||||||
|
|
||||||
interpolated_config = interpolate_environment_variables(service_dicts, 'service')
|
if config_file.version == V2_0:
|
||||||
|
|
||||||
if config_file.version == 2:
|
|
||||||
processed_config = dict(config_file.config)
|
processed_config = dict(config_file.config)
|
||||||
processed_config['services'] = interpolated_config
|
processed_config['services'] = services
|
||||||
processed_config['volumes'] = interpolate_environment_variables(
|
processed_config['volumes'] = interpolate_config_section(
|
||||||
config_file.get_volumes(), 'volume')
|
config_file.filename,
|
||||||
processed_config['networks'] = interpolate_environment_variables(
|
config_file.get_volumes(),
|
||||||
config_file.get_networks(), 'network')
|
'volume')
|
||||||
|
processed_config['networks'] = interpolate_config_section(
|
||||||
|
config_file.filename,
|
||||||
|
config_file.get_networks(),
|
||||||
|
'network')
|
||||||
|
|
||||||
if config_file.version == 1:
|
if config_file.version == V1:
|
||||||
processed_config = interpolated_config
|
processed_config = services
|
||||||
|
|
||||||
config_file = config_file._replace(config=processed_config)
|
config_file = config_file._replace(config=processed_config)
|
||||||
validate_against_fields_schema(config_file)
|
validate_against_fields_schema(config_file)
|
||||||
|
|
||||||
if service_name and service_name not in processed_config:
|
if service_name and service_name not in services:
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
"Cannot extend service '{}' in {}: Service not found".format(
|
"Cannot extend service '{}' in {}: Service not found".format(
|
||||||
service_name, config_file.filename))
|
service_name, config_file.filename))
|
||||||
|
|
@ -373,11 +425,11 @@ def process_config_file(config_file, service_name=None):
|
||||||
|
|
||||||
|
|
||||||
class ServiceExtendsResolver(object):
|
class ServiceExtendsResolver(object):
|
||||||
def __init__(self, service_config, version, 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.version = version
|
self.config_file = config_file
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def signature(self):
|
def signature(self):
|
||||||
|
|
@ -404,10 +456,13 @@ class ServiceExtendsResolver(object):
|
||||||
config_path = self.get_extended_config_path(extends)
|
config_path = self.get_extended_config_path(extends)
|
||||||
service_name = extends['service']
|
service_name = extends['service']
|
||||||
|
|
||||||
|
extends_file = ConfigFile.from_filename(config_path)
|
||||||
|
validate_config_version([self.config_file, extends_file])
|
||||||
extended_file = process_config_file(
|
extended_file = process_config_file(
|
||||||
ConfigFile.from_filename(config_path),
|
extends_file,
|
||||||
service_name=service_name)
|
service_name=service_name)
|
||||||
service_config = extended_file.config[service_name]
|
service_config = extended_file.get_service(service_name)
|
||||||
|
|
||||||
return config_path, service_config, service_name
|
return config_path, service_config, service_name
|
||||||
|
|
||||||
def resolve_extends(self, extended_config_path, service_dict, service_name):
|
def resolve_extends(self, extended_config_path, service_dict, service_name):
|
||||||
|
|
@ -417,7 +472,7 @@ class ServiceExtendsResolver(object):
|
||||||
extended_config_path,
|
extended_config_path,
|
||||||
service_name,
|
service_name,
|
||||||
service_dict),
|
service_dict),
|
||||||
self.version,
|
self.config_file,
|
||||||
already_seen=self.already_seen + [self.signature])
|
already_seen=self.already_seen + [self.signature])
|
||||||
|
|
||||||
service_config = resolver.run()
|
service_config = resolver.run()
|
||||||
|
|
@ -425,13 +480,12 @@ class ServiceExtendsResolver(object):
|
||||||
validate_extended_service_dict(
|
validate_extended_service_dict(
|
||||||
other_service_dict,
|
other_service_dict,
|
||||||
extended_config_path,
|
extended_config_path,
|
||||||
service_name,
|
service_name)
|
||||||
)
|
|
||||||
|
|
||||||
return merge_service_dicts(
|
return merge_service_dicts(
|
||||||
other_service_dict,
|
other_service_dict,
|
||||||
self.service_config.config,
|
self.service_config.config,
|
||||||
self.version)
|
self.config_file.version)
|
||||||
|
|
||||||
def get_extended_config_path(self, extends_options):
|
def get_extended_config_path(self, extends_options):
|
||||||
"""Service we are extending either has a value for 'file' set, which we
|
"""Service we are extending either has a value for 'file' set, which we
|
||||||
|
|
@ -477,26 +531,28 @@ def validate_extended_service_dict(service_dict, filename, service):
|
||||||
"%s services with 'volumes_from' cannot be extended" % error_prefix)
|
"%s services with 'volumes_from' cannot be extended" % error_prefix)
|
||||||
|
|
||||||
if 'net' in service_dict:
|
if 'net' in service_dict:
|
||||||
if get_service_name_from_net(service_dict['net']) is not None:
|
if get_container_name_from_network_mode(service_dict['net']):
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
"%s services with 'net: container' cannot be extended" % error_prefix)
|
"%s services with 'net: container' cannot be extended" % error_prefix)
|
||||||
|
|
||||||
|
if 'network_mode' in service_dict:
|
||||||
|
if get_service_name_from_network_mode(service_dict['network_mode']):
|
||||||
|
raise ConfigurationError(
|
||||||
|
"%s services with 'network_mode: service' cannot be extended" % error_prefix)
|
||||||
|
|
||||||
def validate_ulimits(ulimit_config):
|
if 'depends_on' in service_dict:
|
||||||
for limit_name, soft_hard_values in six.iteritems(ulimit_config):
|
raise ConfigurationError(
|
||||||
if isinstance(soft_hard_values, dict):
|
"%s services with 'depends_on' cannot be extended" % error_prefix)
|
||||||
if not soft_hard_values['soft'] <= soft_hard_values['hard']:
|
|
||||||
raise ConfigurationError(
|
|
||||||
"ulimit_config \"{}\" cannot contain a 'soft' value higher "
|
|
||||||
"than 'hard' value".format(ulimit_config))
|
|
||||||
|
|
||||||
|
|
||||||
def validate_service(service_dict, service_name, version):
|
def validate_service(service_config, service_names, version):
|
||||||
|
service_dict, service_name = service_config.config, service_config.name
|
||||||
validate_against_service_schema(service_dict, service_name, version)
|
validate_against_service_schema(service_dict, service_name, version)
|
||||||
validate_paths(service_dict)
|
validate_paths(service_dict)
|
||||||
|
|
||||||
if 'ulimits' in service_dict:
|
validate_ulimits(service_config)
|
||||||
validate_ulimits(service_dict['ulimits'])
|
validate_network_mode(service_config, service_names)
|
||||||
|
validate_depends_on(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(
|
||||||
|
|
@ -556,6 +612,17 @@ def finalize_service(service_config, service_names, version):
|
||||||
service_dict['volumes'] = [
|
service_dict['volumes'] = [
|
||||||
VolumeSpec.parse(v) for v in service_dict['volumes']]
|
VolumeSpec.parse(v) for v in service_dict['volumes']]
|
||||||
|
|
||||||
|
if 'net' in service_dict:
|
||||||
|
network_mode = service_dict.pop('net')
|
||||||
|
container_name = get_container_name_from_network_mode(network_mode)
|
||||||
|
if container_name and container_name in service_names:
|
||||||
|
service_dict['network_mode'] = 'service:{}'.format(container_name)
|
||||||
|
else:
|
||||||
|
service_dict['network_mode'] = network_mode
|
||||||
|
|
||||||
|
if 'networks' in service_dict:
|
||||||
|
service_dict['networks'] = parse_networks(service_dict['networks'])
|
||||||
|
|
||||||
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'])
|
||||||
|
|
||||||
|
|
@ -598,63 +665,94 @@ def merge_service_dicts_from_files(base, override, version):
|
||||||
return new_service
|
return new_service
|
||||||
|
|
||||||
|
|
||||||
|
class MergeDict(dict):
|
||||||
|
"""A dict-like object responsible for merging two dicts into one."""
|
||||||
|
|
||||||
|
def __init__(self, base, override):
|
||||||
|
self.base = base
|
||||||
|
self.override = override
|
||||||
|
|
||||||
|
def needs_merge(self, field):
|
||||||
|
return field in self.base or field in self.override
|
||||||
|
|
||||||
|
def merge_field(self, field, merge_func, default=None):
|
||||||
|
if not self.needs_merge(field):
|
||||||
|
return
|
||||||
|
|
||||||
|
self[field] = merge_func(
|
||||||
|
self.base.get(field, default),
|
||||||
|
self.override.get(field, default))
|
||||||
|
|
||||||
|
def merge_mapping(self, field, parse_func):
|
||||||
|
if not self.needs_merge(field):
|
||||||
|
return
|
||||||
|
|
||||||
|
self[field] = parse_func(self.base.get(field))
|
||||||
|
self[field].update(parse_func(self.override.get(field)))
|
||||||
|
|
||||||
|
def merge_sequence(self, field, parse_func):
|
||||||
|
def parse_sequence_func(seq):
|
||||||
|
return to_mapping((parse_func(item) for item in seq), 'merge_field')
|
||||||
|
|
||||||
|
if not self.needs_merge(field):
|
||||||
|
return
|
||||||
|
|
||||||
|
merged = parse_sequence_func(self.base.get(field, []))
|
||||||
|
merged.update(parse_sequence_func(self.override.get(field, [])))
|
||||||
|
self[field] = [item.repr() for item in merged.values()]
|
||||||
|
|
||||||
|
def merge_scalar(self, field):
|
||||||
|
if self.needs_merge(field):
|
||||||
|
self[field] = self.override.get(field, self.base.get(field))
|
||||||
|
|
||||||
|
|
||||||
def merge_service_dicts(base, override, version):
|
def merge_service_dicts(base, override, version):
|
||||||
d = {}
|
md = MergeDict(base, override)
|
||||||
|
|
||||||
def merge_field(field, merge_func, default=None):
|
md.merge_mapping('environment', parse_environment)
|
||||||
if field in base or field in override:
|
md.merge_mapping('labels', parse_labels)
|
||||||
d[field] = merge_func(
|
md.merge_mapping('ulimits', parse_ulimits)
|
||||||
base.get(field, default),
|
md.merge_mapping('networks', parse_networks)
|
||||||
override.get(field, default))
|
md.merge_sequence('links', ServiceLink.parse)
|
||||||
|
|
||||||
def merge_mapping(mapping, parse_func):
|
|
||||||
if mapping in base or mapping in override:
|
|
||||||
merged = parse_func(base.get(mapping, None))
|
|
||||||
merged.update(parse_func(override.get(mapping, None)))
|
|
||||||
d[mapping] = merged
|
|
||||||
|
|
||||||
merge_mapping('environment', parse_environment)
|
|
||||||
merge_mapping('labels', parse_labels)
|
|
||||||
merge_mapping('ulimits', parse_ulimits)
|
|
||||||
|
|
||||||
for field in ['volumes', 'devices']:
|
for field in ['volumes', 'devices']:
|
||||||
merge_field(field, merge_path_mappings)
|
md.merge_field(field, merge_path_mappings)
|
||||||
|
|
||||||
for field in ['ports', 'expose', 'external_links']:
|
for field in [
|
||||||
merge_field(field, operator.add, default=[])
|
'depends_on',
|
||||||
|
'expose',
|
||||||
|
'external_links',
|
||||||
|
'ports',
|
||||||
|
'volumes_from',
|
||||||
|
]:
|
||||||
|
md.merge_field(field, operator.add, default=[])
|
||||||
|
|
||||||
for field in ['dns', 'dns_search', 'env_file']:
|
for field in ['dns', 'dns_search', 'env_file']:
|
||||||
merge_field(field, merge_list_or_string)
|
md.merge_field(field, merge_list_or_string)
|
||||||
|
|
||||||
for field in set(ALLOWED_KEYS) - set(d):
|
for field in set(ALLOWED_KEYS) - set(md):
|
||||||
if field in base or field in override:
|
md.merge_scalar(field)
|
||||||
d[field] = override.get(field, base.get(field))
|
|
||||||
|
|
||||||
if version == 1:
|
if version == V1:
|
||||||
legacy_v1_merge_image_or_build(d, base, override)
|
legacy_v1_merge_image_or_build(md, base, override)
|
||||||
else:
|
elif md.needs_merge('build'):
|
||||||
merge_build(d, base, override)
|
md['build'] = merge_build(md, base, override)
|
||||||
|
|
||||||
return d
|
return dict(md)
|
||||||
|
|
||||||
|
|
||||||
def merge_build(output, base, override):
|
def merge_build(output, base, override):
|
||||||
build = {}
|
def to_dict(service):
|
||||||
|
build_config = service.get('build', {})
|
||||||
|
if isinstance(build_config, six.string_types):
|
||||||
|
return {'context': build_config}
|
||||||
|
return build_config
|
||||||
|
|
||||||
if 'build' in base:
|
md = MergeDict(to_dict(base), to_dict(override))
|
||||||
if isinstance(base['build'], six.string_types):
|
md.merge_scalar('context')
|
||||||
build['context'] = base['build']
|
md.merge_scalar('dockerfile')
|
||||||
else:
|
md.merge_mapping('args', parse_build_arguments)
|
||||||
build.update(base['build'])
|
return dict(md)
|
||||||
|
|
||||||
if 'build' in override:
|
|
||||||
if isinstance(override['build'], six.string_types):
|
|
||||||
build['context'] = override['build']
|
|
||||||
else:
|
|
||||||
build.update(override['build'])
|
|
||||||
|
|
||||||
if build:
|
|
||||||
output['build'] = build
|
|
||||||
|
|
||||||
|
|
||||||
def legacy_v1_merge_image_or_build(output, base, override):
|
def legacy_v1_merge_image_or_build(output, base, override):
|
||||||
|
|
@ -711,6 +809,7 @@ 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_label, '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')
|
||||||
|
|
||||||
|
|
||||||
def parse_ulimits(ulimits):
|
def parse_ulimits(ulimits):
|
||||||
|
|
@ -727,7 +826,7 @@ def resolve_env_var(key, val):
|
||||||
elif key in os.environ:
|
elif key in os.environ:
|
||||||
return key, os.environ[key]
|
return key, os.environ[key]
|
||||||
else:
|
else:
|
||||||
return key, ''
|
return key, None
|
||||||
|
|
||||||
|
|
||||||
def env_vars_from_file(filename):
|
def env_vars_from_file(filename):
|
||||||
|
|
@ -774,7 +873,7 @@ def normalize_build(service_dict, working_dir):
|
||||||
else:
|
else:
|
||||||
build.update(service_dict['build'])
|
build.update(service_dict['build'])
|
||||||
if 'args' in build:
|
if 'args' in build:
|
||||||
build['args'] = resolve_build_args(build)
|
build['args'] = build_string_dict(resolve_build_args(build))
|
||||||
|
|
||||||
service_dict['build'] = build
|
service_dict['build'] = build
|
||||||
|
|
||||||
|
|
@ -797,6 +896,9 @@ def validate_paths(service_dict):
|
||||||
build_path = build
|
build_path = build
|
||||||
elif isinstance(build, dict) and 'context' in build:
|
elif isinstance(build, dict) and 'context' in build:
|
||||||
build_path = build['context']
|
build_path = build['context']
|
||||||
|
else:
|
||||||
|
# We have a build section but no context, so nothing to validate
|
||||||
|
return
|
||||||
|
|
||||||
if (
|
if (
|
||||||
not is_url(build_path) and
|
not is_url(build_path) and
|
||||||
|
|
@ -869,6 +971,10 @@ def to_list(value):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def to_mapping(sequence, key_field):
|
||||||
|
return {getattr(item, key_field): item for item in sequence}
|
||||||
|
|
||||||
|
|
||||||
def has_uppercase(name):
|
def has_uppercase(name):
|
||||||
return any(char in string.ascii_uppercase for char in name)
|
return any(char in string.ascii_uppercase for char in name)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,14 @@ from __future__ import absolute_import
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
|
||||||
|
VERSION_EXPLANATION = (
|
||||||
|
'Either specify a version of "2" (or "2.0") and place your service '
|
||||||
|
'definitions under the `services` key, or omit the `version` key and place '
|
||||||
|
'your service definitions at the root of the file to use version 1.\n'
|
||||||
|
'For more on the Compose file format versions, see '
|
||||||
|
'https://docs.docker.com/compose/compose-file/')
|
||||||
|
|
||||||
|
|
||||||
class ConfigurationError(Exception):
|
class ConfigurationError(Exception):
|
||||||
def __init__(self, msg):
|
def __init__(self, msg):
|
||||||
self.msg = msg
|
self.msg = msg
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
{
|
{
|
||||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"id": "fields_schema_v2.json",
|
"id": "fields_schema_v2.0.json",
|
||||||
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"version": {
|
"version": {
|
||||||
"enum": [2]
|
"type": "string"
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"id": "#/properties/services",
|
"id": "#/properties/services",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"patternProperties": {
|
"patternProperties": {
|
||||||
"^[a-zA-Z0-9._-]+$": {
|
"^[a-zA-Z0-9._-]+$": {
|
||||||
"$ref": "service_schema_v2.json#/definitions/service"
|
"$ref": "service_schema_v2.0.json#/definitions/service"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|
@ -41,7 +41,34 @@
|
||||||
"definitions": {
|
"definitions": {
|
||||||
"network": {
|
"network": {
|
||||||
"id": "#/definitions/network",
|
"id": "#/definitions/network",
|
||||||
"type": "object"
|
"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": {
|
"volume": {
|
||||||
"id": "#/definitions/volume",
|
"id": "#/definitions/volume",
|
||||||
|
|
@ -21,7 +21,7 @@ def interpolate_environment_variables(config, section):
|
||||||
)
|
)
|
||||||
|
|
||||||
return dict(
|
return dict(
|
||||||
(name, process_item(name, config_dict))
|
(name, process_item(name, config_dict or {}))
|
||||||
for name, config_dict in config.items()
|
for name, config_dict in config.items()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||||
"id": "service_schema_v2.json",
|
"id": "service_schema_v2.0.json",
|
||||||
|
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|
||||||
|
|
@ -23,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
|
||||||
}
|
}
|
||||||
|
|
@ -42,6 +55,7 @@
|
||||||
"cpu_shares": {"type": ["number", "string"]},
|
"cpu_shares": {"type": ["number", "string"]},
|
||||||
"cpu_quota": {"type": ["number", "string"]},
|
"cpu_quota": {"type": ["number", "string"]},
|
||||||
"cpuset": {"type": "string"},
|
"cpuset": {"type": "string"},
|
||||||
|
"depends_on": {"$ref": "#/definitions/list_of_strings"},
|
||||||
"devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
"devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||||
"dns": {"$ref": "#/definitions/string_or_list"},
|
"dns": {"$ref": "#/definitions/string_or_list"},
|
||||||
"dns_search": {"$ref": "#/definitions/string_or_list"},
|
"dns_search": {"$ref": "#/definitions/string_or_list"},
|
||||||
|
|
@ -82,11 +96,13 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||||
"extra_hosts": {"$ref": "#/definitions/list_or_dict"},
|
"extra_hosts": {"$ref": "#/definitions/list_or_dict"},
|
||||||
"hostname": {"type": "string"},
|
"hostname": {"type": "string"},
|
||||||
"image": {"type": "string"},
|
"image": {"type": "string"},
|
||||||
"ipc": {"type": "string"},
|
"ipc": {"type": "string"},
|
||||||
"labels": {"$ref": "#/definitions/list_or_dict"},
|
"labels": {"$ref": "#/definitions/list_or_dict"},
|
||||||
|
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||||
|
|
||||||
"logging": {
|
"logging": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|
@ -101,13 +117,31 @@
|
||||||
"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"]},
|
||||||
|
"network_mode": {"type": "string"},
|
||||||
|
|
||||||
"networks": {
|
"networks": {
|
||||||
"type": "array",
|
"oneOf": [
|
||||||
"items": {"type": "string"},
|
{"$ref": "#/definitions/list_of_strings"},
|
||||||
"uniqueItems": true
|
{
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
"^[a-zA-Z0-9._-]+$": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"aliases": {"$ref": "#/definitions/list_of_strings"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{"type": "null"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
"pid": {"type": ["string", "null"]},
|
"pid": {"type": ["string", "null"]},
|
||||||
|
|
||||||
"ports": {
|
"ports": {
|
||||||
|
|
@ -191,7 +225,12 @@
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{"required": ["build"]},
|
{"required": ["build"]},
|
||||||
{"required": ["image"]}
|
{"required": ["image"]}
|
||||||
]
|
],
|
||||||
|
"properties": {
|
||||||
|
"build": {
|
||||||
|
"required": ["context"]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,14 +4,22 @@ from __future__ import unicode_literals
|
||||||
from compose.config.errors import DependencyError
|
from compose.config.errors import DependencyError
|
||||||
|
|
||||||
|
|
||||||
def get_service_name_from_net(net_config):
|
def get_service_name_from_network_mode(network_mode):
|
||||||
if not net_config:
|
return get_source_name_from_network_mode(network_mode, 'service')
|
||||||
|
|
||||||
|
|
||||||
|
def get_container_name_from_network_mode(network_mode):
|
||||||
|
return get_source_name_from_network_mode(network_mode, 'container')
|
||||||
|
|
||||||
|
|
||||||
|
def get_source_name_from_network_mode(network_mode, source_type):
|
||||||
|
if not network_mode:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not net_config.startswith('container:'):
|
if not network_mode.startswith(source_type+':'):
|
||||||
return
|
return
|
||||||
|
|
||||||
_, net_name = net_config.split(':', 1)
|
_, net_name = network_mode.split(':', 1)
|
||||||
return net_name
|
return net_name
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -33,7 +41,8 @@ def sort_service_dicts(services):
|
||||||
service for service in services
|
service for service in services
|
||||||
if (name in get_service_names(service.get('links', [])) or
|
if (name in get_service_names(service.get('links', [])) or
|
||||||
name in get_service_names_from_volumes_from(service.get('volumes_from', [])) or
|
name in get_service_names_from_volumes_from(service.get('volumes_from', [])) or
|
||||||
name == get_service_name_from_net(service.get('net')))
|
name == get_service_name_from_network_mode(service.get('network_mode')) or
|
||||||
|
name in service.get('depends_on', []))
|
||||||
]
|
]
|
||||||
|
|
||||||
def visit(n):
|
def visit(n):
|
||||||
|
|
@ -42,8 +51,10 @@ def sort_service_dicts(services):
|
||||||
raise DependencyError('A service can not link to itself: %s' % n['name'])
|
raise DependencyError('A service can not link to itself: %s' % n['name'])
|
||||||
if n['name'] in n.get('volumes_from', []):
|
if n['name'] in n.get('volumes_from', []):
|
||||||
raise DependencyError('A service can not mount itself as volume: %s' % n['name'])
|
raise DependencyError('A service can not mount itself as volume: %s' % n['name'])
|
||||||
else:
|
if n['name'] in n.get('depends_on', []):
|
||||||
raise DependencyError('Circular import between %s' % ' and '.join(temporary_marked))
|
raise DependencyError('A service can not depend on itself: %s' % n['name'])
|
||||||
|
raise DependencyError('Circular dependency between %s' % ' and '.join(temporary_marked))
|
||||||
|
|
||||||
if n in unmarked:
|
if n in unmarked:
|
||||||
temporary_marked.add(n['name'])
|
temporary_marked.add(n['name'])
|
||||||
for m in get_service_dependents(n, services):
|
for m in get_service_dependents(n, services):
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ from __future__ import unicode_literals
|
||||||
import os
|
import os
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
|
from compose.config.config import V1
|
||||||
from compose.config.errors import ConfigurationError
|
from compose.config.errors import ConfigurationError
|
||||||
from compose.const import IS_WINDOWS_PLATFORM
|
from compose.const import IS_WINDOWS_PLATFORM
|
||||||
|
|
||||||
|
|
@ -16,7 +17,7 @@ class VolumeFromSpec(namedtuple('_VolumeFromSpec', 'source mode type')):
|
||||||
# TODO: drop service_names arg when v1 is removed
|
# TODO: drop service_names arg when v1 is removed
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse(cls, volume_from_config, service_names, version):
|
def parse(cls, volume_from_config, service_names, version):
|
||||||
func = cls.parse_v1 if version == 1 else cls.parse_v2
|
func = cls.parse_v1 if version == V1 else cls.parse_v2
|
||||||
return func(service_names, volume_from_config)
|
return func(service_names, volume_from_config)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -163,3 +164,26 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')):
|
||||||
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
|
||||||
|
def is_named_volume(self):
|
||||||
|
return self.external and not self.external.startswith(('.', '/', '~'))
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceLink(namedtuple('_ServiceLink', 'target alias')):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, link_spec):
|
||||||
|
target, _, alias = link_spec.partition(':')
|
||||||
|
if not alias:
|
||||||
|
alias = target
|
||||||
|
return cls(target, alias)
|
||||||
|
|
||||||
|
def repr(self):
|
||||||
|
if self.target == self.alias:
|
||||||
|
return self.target
|
||||||
|
return '{s.target}:{s.alias}'.format(s=self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def merge_field(self):
|
||||||
|
return self.alias
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ from jsonschema import RefResolver
|
||||||
from jsonschema import ValidationError
|
from jsonschema import ValidationError
|
||||||
|
|
||||||
from .errors import ConfigurationError
|
from .errors import ConfigurationError
|
||||||
|
from .errors import VERSION_EXPLANATION
|
||||||
|
from .sort_services import get_service_name_from_network_mode
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
@ -62,46 +64,78 @@ def format_expose(instance):
|
||||||
|
|
||||||
@FormatChecker.cls_checks(format="bool-value-in-mapping")
|
@FormatChecker.cls_checks(format="bool-value-in-mapping")
|
||||||
def format_boolean_in_environment(instance):
|
def format_boolean_in_environment(instance):
|
||||||
"""
|
"""Check if there is a boolean in the mapping sections and display a warning.
|
||||||
Check if there is a boolean in the environment and display a warning.
|
|
||||||
Always return True here so the validation won't raise an error.
|
Always return True here so the validation won't raise an error.
|
||||||
"""
|
"""
|
||||||
if isinstance(instance, bool):
|
if isinstance(instance, bool):
|
||||||
log.warn(
|
log.warn(
|
||||||
"There is a boolean value in the 'environment' key.\n"
|
"There is a boolean value in the 'environment', 'labels', or "
|
||||||
"Environment variables can only be strings.\n"
|
"'extra_hosts' field of a service.\n"
|
||||||
"Please add quotes to any boolean values to make them string "
|
"These sections only support string values.\n"
|
||||||
"(eg, 'True', 'yes', 'N').\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"
|
"This warning will become an error in a future release. \r\n"
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def validate_top_level_service_objects(filename, service_dicts):
|
def match_named_volumes(service_dict, project_volumes):
|
||||||
"""Perform some high level validation of the service name and value.
|
service_volumes = service_dict.get('volumes', [])
|
||||||
|
for volume_spec in service_volumes:
|
||||||
This validation must happen before interpolation, which must happen
|
if volume_spec.is_named_volume and volume_spec.external not in project_volumes:
|
||||||
before the rest of validation, which is why it's separate from the
|
|
||||||
rest of the service validation.
|
|
||||||
"""
|
|
||||||
for service_name, service_dict in service_dicts.items():
|
|
||||||
if not isinstance(service_name, six.string_types):
|
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
"In file '{}' service name: {} needs to be a string, eg '{}'".format(
|
'Named volume "{0}" is used in service "{1}" but no'
|
||||||
filename,
|
' declaration was found in the volumes section.'.format(
|
||||||
service_name,
|
volume_spec.repr(), service_dict.get('name')
|
||||||
service_name))
|
|
||||||
|
|
||||||
if not isinstance(service_dict, dict):
|
|
||||||
raise ConfigurationError(
|
|
||||||
"In file '{}' service '{}' doesn\'t have any configuration options. "
|
|
||||||
"All top level keys in your docker-compose.yml must map "
|
|
||||||
"to a dictionary of configuration options.".format(
|
|
||||||
filename, service_name
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def python_type_to_yaml_type(type_):
|
||||||
|
type_name = type(type_).__name__
|
||||||
|
return {
|
||||||
|
'dict': 'mapping',
|
||||||
|
'list': 'array',
|
||||||
|
'int': 'number',
|
||||||
|
'float': 'number',
|
||||||
|
'bool': 'boolean',
|
||||||
|
'unicode': 'string',
|
||||||
|
'str': 'string',
|
||||||
|
'bytes': 'string',
|
||||||
|
}.get(type_name, type_name)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_config_section(filename, config, section):
|
||||||
|
"""Validate the structure of a configuration section. This must be done
|
||||||
|
before interpolation so it's separate from schema validation.
|
||||||
|
"""
|
||||||
|
if not isinstance(config, dict):
|
||||||
|
raise ConfigurationError(
|
||||||
|
"In file '{filename}', {section} must be a mapping, not "
|
||||||
|
"{type}.".format(
|
||||||
|
filename=filename,
|
||||||
|
section=section,
|
||||||
|
type=anglicize_json_type(python_type_to_yaml_type(config))))
|
||||||
|
|
||||||
|
for key, value in config.items():
|
||||||
|
if not isinstance(key, six.string_types):
|
||||||
|
raise ConfigurationError(
|
||||||
|
"In file '{filename}', the {section} name {name} must be a "
|
||||||
|
"quoted string, i.e. '{name}'.".format(
|
||||||
|
filename=filename,
|
||||||
|
section=section,
|
||||||
|
name=key))
|
||||||
|
|
||||||
|
if not isinstance(value, (dict, type(None))):
|
||||||
|
raise ConfigurationError(
|
||||||
|
"In file '{filename}', {section} '{name}' must be a mapping not "
|
||||||
|
"{type}.".format(
|
||||||
|
filename=filename,
|
||||||
|
section=section,
|
||||||
|
name=key,
|
||||||
|
type=anglicize_json_type(python_type_to_yaml_type(value))))
|
||||||
|
|
||||||
|
|
||||||
def validate_top_level_object(config_file):
|
def validate_top_level_object(config_file):
|
||||||
if not isinstance(config_file.config, dict):
|
if not isinstance(config_file.config, dict):
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
|
|
@ -110,6 +144,18 @@ def validate_top_level_object(config_file):
|
||||||
type(config_file.config)))
|
type(config_file.config)))
|
||||||
|
|
||||||
|
|
||||||
|
def validate_ulimits(service_config):
|
||||||
|
ulimit_config = service_config.config.get('ulimits', {})
|
||||||
|
for limit_name, soft_hard_values in six.iteritems(ulimit_config):
|
||||||
|
if isinstance(soft_hard_values, dict):
|
||||||
|
if not soft_hard_values['soft'] <= soft_hard_values['hard']:
|
||||||
|
raise ConfigurationError(
|
||||||
|
"Service '{s.name}' has invalid ulimit '{ulimit}'. "
|
||||||
|
"'soft' value can not be greater than 'hard' value ".format(
|
||||||
|
s=service_config,
|
||||||
|
ulimit=ulimit_config))
|
||||||
|
|
||||||
|
|
||||||
def validate_extends_file_path(service_name, extends_options, filename):
|
def validate_extends_file_path(service_name, extends_options, filename):
|
||||||
"""
|
"""
|
||||||
The service to be extended must either be defined in the config key 'file',
|
The service to be extended must either be defined in the config key 'file',
|
||||||
|
|
@ -123,24 +169,50 @@ def validate_extends_file_path(service_name, extends_options, filename):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_unsupported_config_msg(service_name, error_key):
|
def validate_network_mode(service_config, service_names):
|
||||||
msg = "Unsupported config option for '{}' service: '{}'".format(service_name, error_key)
|
network_mode = service_config.config.get('network_mode')
|
||||||
|
if not network_mode:
|
||||||
|
return
|
||||||
|
|
||||||
|
if 'networks' in service_config.config:
|
||||||
|
raise ConfigurationError("'network_mode' and 'networks' cannot be combined")
|
||||||
|
|
||||||
|
dependency = get_service_name_from_network_mode(network_mode)
|
||||||
|
if not dependency:
|
||||||
|
return
|
||||||
|
|
||||||
|
if dependency not in service_names:
|
||||||
|
raise ConfigurationError(
|
||||||
|
"Service '{s.name}' uses the network stack of service '{dep}' which "
|
||||||
|
"is undefined.".format(s=service_config, dep=dependency))
|
||||||
|
|
||||||
|
|
||||||
|
def validate_depends_on(service_config, service_names):
|
||||||
|
for dependency in service_config.config.get('depends_on', []):
|
||||||
|
if dependency not in service_names:
|
||||||
|
raise ConfigurationError(
|
||||||
|
"Service '{s.name}' depends on service '{dep}' which is "
|
||||||
|
"undefined.".format(s=service_config, dep=dependency))
|
||||||
|
|
||||||
|
|
||||||
|
def get_unsupported_config_msg(path, error_key):
|
||||||
|
msg = "Unsupported config option for {}: '{}'".format(path_string(path), error_key)
|
||||||
if error_key in DOCKER_CONFIG_HINTS:
|
if error_key in DOCKER_CONFIG_HINTS:
|
||||||
msg += " (did you mean '{}'?)".format(DOCKER_CONFIG_HINTS[error_key])
|
msg += " (did you mean '{}'?)".format(DOCKER_CONFIG_HINTS[error_key])
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
|
||||||
def anglicize_validator(validator):
|
def anglicize_json_type(json_type):
|
||||||
if validator in ["array", "object"]:
|
if json_type.startswith(('a', 'e', 'i', 'o', 'u')):
|
||||||
return 'an ' + validator
|
return 'an ' + json_type
|
||||||
return 'a ' + validator
|
return 'a ' + json_type
|
||||||
|
|
||||||
|
|
||||||
def is_service_dict_schema(schema_id):
|
def is_service_dict_schema(schema_id):
|
||||||
return schema_id == 'fields_schema_v1.json' or schema_id == '#/properties/services'
|
return schema_id == 'fields_schema_v1.json' or schema_id == '#/properties/services'
|
||||||
|
|
||||||
|
|
||||||
def handle_error_for_schema_with_id(error, service_name):
|
def handle_error_for_schema_with_id(error, path):
|
||||||
schema_id = error.schema['id']
|
schema_id = error.schema['id']
|
||||||
|
|
||||||
if is_service_dict_schema(schema_id) and error.validator == 'additionalProperties':
|
if is_service_dict_schema(schema_id) and error.validator == 'additionalProperties':
|
||||||
|
|
@ -164,62 +236,66 @@ def handle_error_for_schema_with_id(error, service_name):
|
||||||
# TODO: only applies to v1
|
# TODO: only applies to v1
|
||||||
if 'image' in error.instance and context:
|
if 'image' in error.instance and context:
|
||||||
return (
|
return (
|
||||||
"Service '{}' has both an image and build path specified. "
|
"{} has both an image and build path specified. "
|
||||||
"A service can either be built to image or use an existing "
|
"A service can either be built to image or use an existing "
|
||||||
"image, not both.".format(service_name))
|
"image, not both.".format(path_string(path)))
|
||||||
if 'image' not in error.instance and not context:
|
if 'image' not in error.instance and not context:
|
||||||
return (
|
return (
|
||||||
"Service '{}' has neither an image nor a build path "
|
"{} has neither an image nor a build path specified. "
|
||||||
"specified. At least one must be provided.".format(service_name))
|
"At least one must be provided.".format(path_string(path)))
|
||||||
# TODO: only applies to v1
|
# TODO: only applies to v1
|
||||||
if 'image' in error.instance and dockerfile:
|
if 'image' in error.instance and dockerfile:
|
||||||
return (
|
return (
|
||||||
"Service '{}' has both an image and alternate Dockerfile. "
|
"{} has both an image and alternate Dockerfile. "
|
||||||
"A service can either be built to image or use an existing "
|
"A service can either be built to image or use an existing "
|
||||||
"image, not both.".format(service_name))
|
"image, not both.".format(path_string(path)))
|
||||||
|
|
||||||
if schema_id == '#/definitions/service':
|
if error.validator == 'additionalProperties':
|
||||||
if error.validator == 'additionalProperties':
|
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(service_name, invalid_config_key)
|
return get_unsupported_config_msg(path, invalid_config_key)
|
||||||
|
|
||||||
|
if not error.path:
|
||||||
|
return '{}\n{}'.format(error.message, VERSION_EXPLANATION)
|
||||||
|
|
||||||
|
|
||||||
def handle_generic_service_error(error, service_name):
|
def handle_generic_service_error(error, path):
|
||||||
config_key = " ".join("'%s'" % k for k in error.path)
|
|
||||||
msg_format = None
|
msg_format = None
|
||||||
error_msg = error.message
|
error_msg = error.message
|
||||||
|
|
||||||
if error.validator == 'oneOf':
|
if error.validator == 'oneOf':
|
||||||
msg_format = "Service '{}' configuration key {} {}"
|
msg_format = "{path} {msg}"
|
||||||
error_msg = _parse_oneof_validator(error)
|
config_key, error_msg = _parse_oneof_validator(error)
|
||||||
|
if config_key:
|
||||||
|
path.append(config_key)
|
||||||
|
|
||||||
elif error.validator == 'type':
|
elif error.validator == 'type':
|
||||||
msg_format = ("Service '{}' configuration key {} contains an invalid "
|
msg_format = "{path} contains an invalid type, it should be {msg}"
|
||||||
"type, it should be {}")
|
|
||||||
error_msg = _parse_valid_types_from_validator(error.validator_value)
|
error_msg = _parse_valid_types_from_validator(error.validator_value)
|
||||||
|
|
||||||
# TODO: no test case for this branch, there are no config options
|
|
||||||
# which exercise this branch
|
|
||||||
elif error.validator == 'required':
|
elif error.validator == 'required':
|
||||||
msg_format = "Service '{}' configuration key '{}' is invalid, {}"
|
error_msg = ", ".join(error.validator_value)
|
||||||
|
msg_format = "{path} is invalid, {msg} is required."
|
||||||
|
|
||||||
elif error.validator == 'dependencies':
|
elif error.validator == 'dependencies':
|
||||||
msg_format = "Service '{}' configuration key '{}' is invalid: {}"
|
|
||||||
config_key = list(error.validator_value.keys())[0]
|
config_key = list(error.validator_value.keys())[0]
|
||||||
required_keys = ",".join(error.validator_value[config_key])
|
required_keys = ",".join(error.validator_value[config_key])
|
||||||
|
|
||||||
|
msg_format = "{path} is invalid: {msg}"
|
||||||
|
path.append(config_key)
|
||||||
error_msg = "when defining '{}' you must set '{}' as well".format(
|
error_msg = "when defining '{}' you must set '{}' as well".format(
|
||||||
config_key,
|
config_key,
|
||||||
required_keys)
|
required_keys)
|
||||||
|
|
||||||
elif error.cause:
|
elif error.cause:
|
||||||
error_msg = six.text_type(error.cause)
|
error_msg = six.text_type(error.cause)
|
||||||
msg_format = "Service '{}' configuration key {} is invalid: {}"
|
msg_format = "{path} is invalid: {msg}"
|
||||||
|
|
||||||
elif error.path:
|
elif error.path:
|
||||||
msg_format = "Service '{}' configuration key {} value {}"
|
msg_format = "{path} value {msg}"
|
||||||
|
|
||||||
if msg_format:
|
if msg_format:
|
||||||
return msg_format.format(service_name, config_key, error_msg)
|
return msg_format.format(path=path_string(path), msg=error_msg)
|
||||||
|
|
||||||
return error.message
|
return error.message
|
||||||
|
|
||||||
|
|
@ -228,19 +304,23 @@ def parse_key_from_error_msg(error):
|
||||||
return error.message.split("'")[1]
|
return error.message.split("'")[1]
|
||||||
|
|
||||||
|
|
||||||
|
def path_string(path):
|
||||||
|
return ".".join(c for c in path if isinstance(c, six.string_types))
|
||||||
|
|
||||||
|
|
||||||
def _parse_valid_types_from_validator(validator):
|
def _parse_valid_types_from_validator(validator):
|
||||||
"""A validator value can be either an array of valid types or a string of
|
"""A validator value can be either an array of valid types or a string of
|
||||||
a valid type. Parse the valid types and prefix with the correct article.
|
a valid type. Parse the valid types and prefix with the correct article.
|
||||||
"""
|
"""
|
||||||
if not isinstance(validator, list):
|
if not isinstance(validator, list):
|
||||||
return anglicize_validator(validator)
|
return anglicize_json_type(validator)
|
||||||
|
|
||||||
if len(validator) == 1:
|
if len(validator) == 1:
|
||||||
return anglicize_validator(validator[0])
|
return anglicize_json_type(validator[0])
|
||||||
|
|
||||||
return "{}, or {}".format(
|
return "{}, or {}".format(
|
||||||
", ".join([anglicize_validator(validator[0])] + validator[1:-1]),
|
", ".join([anglicize_json_type(validator[0])] + validator[1:-1]),
|
||||||
anglicize_validator(validator[-1]))
|
anglicize_json_type(validator[-1]))
|
||||||
|
|
||||||
|
|
||||||
def _parse_oneof_validator(error):
|
def _parse_oneof_validator(error):
|
||||||
|
|
@ -252,53 +332,57 @@ def _parse_oneof_validator(error):
|
||||||
types = []
|
types = []
|
||||||
for context in error.context:
|
for context in error.context:
|
||||||
|
|
||||||
|
if context.validator == 'oneOf':
|
||||||
|
_, error_msg = _parse_oneof_validator(context)
|
||||||
|
return path_string(context.path), error_msg
|
||||||
|
|
||||||
if context.validator == 'required':
|
if context.validator == 'required':
|
||||||
return context.message
|
return (None, context.message)
|
||||||
|
|
||||||
if context.validator == 'additionalProperties':
|
if context.validator == 'additionalProperties':
|
||||||
invalid_config_key = parse_key_from_error_msg(context)
|
invalid_config_key = parse_key_from_error_msg(context)
|
||||||
return "contains unsupported option: '{}'".format(invalid_config_key)
|
return (None, "contains unsupported option: '{}'".format(invalid_config_key))
|
||||||
|
|
||||||
if context.path:
|
if context.path:
|
||||||
invalid_config_key = " ".join(
|
return (
|
||||||
"'{}' ".format(fragment) for fragment in context.path
|
path_string(context.path),
|
||||||
if isinstance(fragment, six.string_types)
|
"contains {}, which is an invalid type, it should be {}".format(
|
||||||
|
json.dumps(context.instance),
|
||||||
|
_parse_valid_types_from_validator(context.validator_value)),
|
||||||
)
|
)
|
||||||
return "{}contains {}, which is an invalid type, it should be {}".format(
|
|
||||||
invalid_config_key,
|
|
||||||
# Always print the json repr of the invalid value
|
|
||||||
json.dumps(context.instance),
|
|
||||||
_parse_valid_types_from_validator(context.validator_value))
|
|
||||||
|
|
||||||
if context.validator == 'uniqueItems':
|
if context.validator == 'uniqueItems':
|
||||||
return "contains non unique items, please remove duplicates from {}".format(
|
return (
|
||||||
context.instance)
|
None,
|
||||||
|
"contains non unique items, please remove duplicates from {}".format(
|
||||||
|
context.instance),
|
||||||
|
)
|
||||||
|
|
||||||
if context.validator == 'type':
|
if context.validator == 'type':
|
||||||
types.append(context.validator_value)
|
types.append(context.validator_value)
|
||||||
|
|
||||||
valid_types = _parse_valid_types_from_validator(types)
|
valid_types = _parse_valid_types_from_validator(types)
|
||||||
return "contains an invalid type, it should be {}".format(valid_types)
|
return (None, "contains an invalid type, it should be {}".format(valid_types))
|
||||||
|
|
||||||
|
|
||||||
def process_errors(errors, service_name=None):
|
def process_errors(errors, path_prefix=None):
|
||||||
"""jsonschema gives us an error tree full of information to explain what has
|
"""jsonschema gives us 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.
|
||||||
"""
|
"""
|
||||||
def format_error_message(error, service_name):
|
path_prefix = path_prefix or []
|
||||||
if not service_name and error.path:
|
|
||||||
# field_schema errors will have service name on the path
|
def format_error_message(error):
|
||||||
service_name = error.path.popleft()
|
path = path_prefix + list(error.path)
|
||||||
|
|
||||||
if 'id' in error.schema:
|
if 'id' in error.schema:
|
||||||
error_msg = handle_error_for_schema_with_id(error, service_name)
|
error_msg = handle_error_for_schema_with_id(error, path)
|
||||||
if error_msg:
|
if error_msg:
|
||||||
return error_msg
|
return error_msg
|
||||||
|
|
||||||
return handle_generic_service_error(error, service_name)
|
return handle_generic_service_error(error, path)
|
||||||
|
|
||||||
return '\n'.join(format_error_message(error, service_name) for error in errors)
|
return '\n'.join(format_error_message(error) for error in errors)
|
||||||
|
|
||||||
|
|
||||||
def validate_against_fields_schema(config_file):
|
def validate_against_fields_schema(config_file):
|
||||||
|
|
@ -315,14 +399,14 @@ def validate_against_service_schema(config, service_name, version):
|
||||||
config,
|
config,
|
||||||
"service_schema_v{0}.json".format(version),
|
"service_schema_v{0}.json".format(version),
|
||||||
format_checker=["ports"],
|
format_checker=["ports"],
|
||||||
service_name=service_name)
|
path_prefix=[service_name])
|
||||||
|
|
||||||
|
|
||||||
def _validate_against_schema(
|
def _validate_against_schema(
|
||||||
config,
|
config,
|
||||||
schema_filename,
|
schema_filename,
|
||||||
format_checker=(),
|
format_checker=(),
|
||||||
service_name=None,
|
path_prefix=None,
|
||||||
filename=None):
|
filename=None):
|
||||||
config_source_dir = os.path.dirname(os.path.abspath(__file__))
|
config_source_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
|
@ -348,7 +432,7 @@ def _validate_against_schema(
|
||||||
if not errors:
|
if not errors:
|
||||||
return
|
return
|
||||||
|
|
||||||
error_msg = process_errors(errors, service_name)
|
error_msg = process_errors(errors, path_prefix=path_prefix)
|
||||||
file_msg = " in file '{}'".format(filename) if filename else ''
|
file_msg = " in file '{}'".format(filename) if filename else ''
|
||||||
raise ConfigurationError("Validation failed{}, reason(s):\n{}".format(
|
raise ConfigurationError("Validation failed{}, reason(s):\n{}".format(
|
||||||
file_msg,
|
file_msg,
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,16 @@ LABEL_PROJECT = 'com.docker.compose.project'
|
||||||
LABEL_SERVICE = 'com.docker.compose.service'
|
LABEL_SERVICE = 'com.docker.compose.service'
|
||||||
LABEL_VERSION = 'com.docker.compose.version'
|
LABEL_VERSION = 'com.docker.compose.version'
|
||||||
LABEL_CONFIG_HASH = 'com.docker.compose.config-hash'
|
LABEL_CONFIG_HASH = 'com.docker.compose.config-hash'
|
||||||
COMPOSEFILE_VERSIONS = (1, 2)
|
|
||||||
|
COMPOSEFILE_V1 = '1'
|
||||||
|
COMPOSEFILE_V2_0 = '2.0'
|
||||||
|
|
||||||
API_VERSIONS = {
|
API_VERSIONS = {
|
||||||
1: '1.21',
|
COMPOSEFILE_V1: '1.21',
|
||||||
2: '1.22',
|
COMPOSEFILE_V2_0: '1.22',
|
||||||
|
}
|
||||||
|
|
||||||
|
API_VERSION_TO_ENGINE_VERSION = {
|
||||||
|
API_VERSIONS[COMPOSEFILE_V1]: '1.9.0',
|
||||||
|
API_VERSIONS[COMPOSEFILE_V2_0]: '1.10.0'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ class Container(object):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def short_id(self):
|
def short_id(self):
|
||||||
return self.id[:10]
|
return self.id[:12]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
|
@ -134,7 +134,11 @@ class Container(object):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def environment(self):
|
def environment(self):
|
||||||
return dict(var.split("=", 1) for var in self.get('Config.Env') or [])
|
def parse_env(var):
|
||||||
|
if '=' in var:
|
||||||
|
return var.split("=", 1)
|
||||||
|
return var, None
|
||||||
|
return dict(parse_env(var) for var in self.get('Config.Env') or [])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def exit_code(self):
|
def exit_code(self):
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ from __future__ import unicode_literals
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from docker.errors import NotFound
|
from docker.errors import NotFound
|
||||||
|
from docker.utils import create_ipam_config
|
||||||
|
from docker.utils import create_ipam_pool
|
||||||
|
|
||||||
from .config import ConfigurationError
|
from .config import ConfigurationError
|
||||||
|
|
||||||
|
|
@ -13,12 +15,13 @@ log = logging.getLogger(__name__)
|
||||||
|
|
||||||
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,
|
||||||
external_name=None):
|
ipam=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.ipam = create_ipam_config_from_dict(ipam)
|
||||||
self.external_name = external_name
|
self.external_name = external_name
|
||||||
|
|
||||||
def ensure(self):
|
def ensure(self):
|
||||||
|
|
@ -44,11 +47,11 @@ class Network(object):
|
||||||
data = self.inspect()
|
data = self.inspect()
|
||||||
if self.driver and data['Driver'] != self.driver:
|
if self.driver and data['Driver'] != self.driver:
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
'Network {} needs to be recreated - driver has changed'
|
'Network "{}" needs to be recreated - driver has changed'
|
||||||
.format(self.full_name))
|
.format(self.full_name))
|
||||||
if data['Options'] != (self.driver_opts or {}):
|
if data['Options'] != (self.driver_opts or {}):
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
'Network {} needs to be recreated - options have changed'
|
'Network "{}" needs to be recreated - options have changed'
|
||||||
.format(self.full_name))
|
.format(self.full_name))
|
||||||
except NotFound:
|
except NotFound:
|
||||||
driver_name = 'the default driver'
|
driver_name = 'the default driver'
|
||||||
|
|
@ -61,7 +64,10 @@ class Network(object):
|
||||||
)
|
)
|
||||||
|
|
||||||
self.client.create_network(
|
self.client.create_network(
|
||||||
self.full_name, self.driver, self.driver_opts
|
name=self.full_name,
|
||||||
|
driver=self.driver,
|
||||||
|
options=self.driver_opts,
|
||||||
|
ipam=self.ipam,
|
||||||
)
|
)
|
||||||
|
|
||||||
def remove(self):
|
def remove(self):
|
||||||
|
|
@ -80,3 +86,102 @@ class Network(object):
|
||||||
if self.external_name:
|
if self.external_name:
|
||||||
return self.external_name
|
return self.external_name
|
||||||
return '{0}_{1}'.format(self.project, self.name)
|
return '{0}_{1}'.format(self.project, self.name)
|
||||||
|
|
||||||
|
|
||||||
|
def create_ipam_config_from_dict(ipam_dict):
|
||||||
|
if not ipam_dict:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return create_ipam_config(
|
||||||
|
driver=ipam_dict.get('driver'),
|
||||||
|
pool_configs=[
|
||||||
|
create_ipam_pool(
|
||||||
|
subnet=config.get('subnet'),
|
||||||
|
iprange=config.get('ip_range'),
|
||||||
|
gateway=config.get('gateway'),
|
||||||
|
aux_addresses=config.get('aux_addresses'),
|
||||||
|
)
|
||||||
|
for config in ipam_dict.get('config', [])
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_networks(name, config_data, client):
|
||||||
|
network_config = config_data.networks or {}
|
||||||
|
networks = {
|
||||||
|
network_name: Network(
|
||||||
|
client=client, project=name, name=network_name,
|
||||||
|
driver=data.get('driver'),
|
||||||
|
driver_opts=data.get('driver_opts'),
|
||||||
|
ipam=data.get('ipam'),
|
||||||
|
external_name=data.get('external_name'),
|
||||||
|
)
|
||||||
|
for network_name, data in network_config.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
if 'default' not in networks:
|
||||||
|
networks['default'] = Network(client, name, 'default')
|
||||||
|
|
||||||
|
return networks
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectNetworks(object):
|
||||||
|
|
||||||
|
def __init__(self, networks, use_networking):
|
||||||
|
self.networks = networks or {}
|
||||||
|
self.use_networking = use_networking
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_services(cls, services, networks, use_networking):
|
||||||
|
service_networks = {
|
||||||
|
network: networks.get(network)
|
||||||
|
for service in services
|
||||||
|
for network in get_network_names_for_service(service)
|
||||||
|
}
|
||||||
|
unused = set(networks) - set(service_networks) - {'default'}
|
||||||
|
if unused:
|
||||||
|
log.warn(
|
||||||
|
"Some networks were defined but are not used by any service: "
|
||||||
|
"{}".format(", ".join(unused)))
|
||||||
|
return cls(service_networks, use_networking)
|
||||||
|
|
||||||
|
def remove(self):
|
||||||
|
if not self.use_networking:
|
||||||
|
return
|
||||||
|
for network in self.networks.values():
|
||||||
|
network.remove()
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
if not self.use_networking:
|
||||||
|
return
|
||||||
|
|
||||||
|
for network in self.networks.values():
|
||||||
|
network.ensure()
|
||||||
|
|
||||||
|
|
||||||
|
def get_network_aliases_for_service(service_dict):
|
||||||
|
if 'network_mode' in service_dict:
|
||||||
|
return {}
|
||||||
|
networks = service_dict.get('networks', {'default': None})
|
||||||
|
return dict(
|
||||||
|
(net, (config or {}).get('aliases', []))
|
||||||
|
for net, config in networks.items()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_network_names_for_service(service_dict):
|
||||||
|
return get_network_aliases_for_service(service_dict).keys()
|
||||||
|
|
||||||
|
|
||||||
|
def get_networks(service_dict, network_definitions):
|
||||||
|
networks = {}
|
||||||
|
for name, aliases in get_network_aliases_for_service(service_dict).items():
|
||||||
|
network = network_definitions.get(name)
|
||||||
|
if network:
|
||||||
|
networks[network.full_name] = aliases
|
||||||
|
else:
|
||||||
|
raise ConfigurationError(
|
||||||
|
'Service "{}" uses an undefined network "{}"'
|
||||||
|
.format(service_dict['name'], name))
|
||||||
|
|
||||||
|
return networks
|
||||||
|
|
|
||||||
|
|
@ -6,25 +6,28 @@ import logging
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
from docker.errors import APIError
|
from docker.errors import APIError
|
||||||
from docker.errors import NotFound
|
|
||||||
|
|
||||||
from . import parallel
|
from . import parallel
|
||||||
from .config import ConfigurationError
|
from .config import ConfigurationError
|
||||||
from .config.sort_services import get_service_name_from_net
|
from .config.config import V1
|
||||||
|
from .config.sort_services import get_container_name_from_network_mode
|
||||||
|
from .config.sort_services import get_service_name_from_network_mode
|
||||||
from .const import DEFAULT_TIMEOUT
|
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
|
||||||
from .const import LABEL_SERVICE
|
from .const import LABEL_SERVICE
|
||||||
from .container import Container
|
from .container import Container
|
||||||
from .network import Network
|
from .network import build_networks
|
||||||
from .service import ContainerNet
|
from .network import get_networks
|
||||||
|
from .network import ProjectNetworks
|
||||||
|
from .service import ContainerNetworkMode
|
||||||
from .service import ConvergenceStrategy
|
from .service import ConvergenceStrategy
|
||||||
from .service import Net
|
from .service import NetworkMode
|
||||||
from .service import Service
|
from .service import Service
|
||||||
from .service import ServiceNet
|
from .service import ServiceNetworkMode
|
||||||
from .utils import microseconds_from_time_nano
|
from .utils import microseconds_from_time_nano
|
||||||
from .volume import Volume
|
from .volume import ProjectVolumes
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
@ -34,15 +37,12 @@ class Project(object):
|
||||||
"""
|
"""
|
||||||
A collection of services.
|
A collection of services.
|
||||||
"""
|
"""
|
||||||
def __init__(self, name, services, client, networks=None, volumes=None,
|
def __init__(self, name, services, client, networks=None, volumes=None):
|
||||||
use_networking=False, network_driver=None):
|
|
||||||
self.name = name
|
self.name = name
|
||||||
self.services = services
|
self.services = services
|
||||||
self.client = client
|
self.client = client
|
||||||
self.use_networking = use_networking
|
self.volumes = volumes or ProjectVolumes({})
|
||||||
self.network_driver = network_driver
|
self.networks = networks or ProjectNetworks({}, False)
|
||||||
self.networks = networks or []
|
|
||||||
self.volumes = volumes or []
|
|
||||||
|
|
||||||
def labels(self, one_off=False):
|
def labels(self, one_off=False):
|
||||||
return [
|
return [
|
||||||
|
|
@ -55,60 +55,47 @@ class Project(object):
|
||||||
"""
|
"""
|
||||||
Construct a Project from a config.Config object.
|
Construct a Project from a config.Config object.
|
||||||
"""
|
"""
|
||||||
use_networking = (config_data.version and config_data.version >= 2)
|
use_networking = (config_data.version and config_data.version != V1)
|
||||||
project = cls(name, [], client, use_networking=use_networking)
|
networks = build_networks(name, config_data, client)
|
||||||
|
project_networks = ProjectNetworks.from_services(
|
||||||
custom_networks = []
|
config_data.services,
|
||||||
if config_data.networks:
|
networks,
|
||||||
for network_name, data in config_data.networks.items():
|
use_networking)
|
||||||
custom_networks.append(
|
volumes = ProjectVolumes.from_config(name, config_data, client)
|
||||||
Network(
|
project = cls(name, [], client, project_networks, volumes)
|
||||||
client=client, project=name, name=network_name,
|
|
||||||
driver=data.get('driver'),
|
|
||||||
driver_opts=data.get('driver_opts'),
|
|
||||||
external_name=data.get('external_name'),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
for service_dict in config_data.services:
|
for service_dict in config_data.services:
|
||||||
|
service_dict = dict(service_dict)
|
||||||
if use_networking:
|
if use_networking:
|
||||||
networks = get_networks(
|
service_networks = get_networks(service_dict, networks)
|
||||||
service_dict,
|
|
||||||
custom_networks + [project.default_network])
|
|
||||||
net = Net(networks[0]) if networks else Net("none")
|
|
||||||
links = []
|
|
||||||
else:
|
else:
|
||||||
networks = []
|
service_networks = {}
|
||||||
net = project.get_net(service_dict)
|
|
||||||
links = project.get_links(service_dict)
|
|
||||||
|
|
||||||
|
service_dict.pop('networks', None)
|
||||||
|
links = project.get_links(service_dict)
|
||||||
|
network_mode = project.get_network_mode(
|
||||||
|
service_dict, list(service_networks.keys())
|
||||||
|
)
|
||||||
volumes_from = get_volumes_from(project, service_dict)
|
volumes_from = get_volumes_from(project, service_dict)
|
||||||
|
|
||||||
|
if config_data.version != V1:
|
||||||
|
service_dict['volumes'] = [
|
||||||
|
volumes.namespace_spec(volume_spec)
|
||||||
|
for volume_spec in service_dict.get('volumes', [])
|
||||||
|
]
|
||||||
|
|
||||||
project.services.append(
|
project.services.append(
|
||||||
Service(
|
Service(
|
||||||
|
service_dict.pop('name'),
|
||||||
client=client,
|
client=client,
|
||||||
project=name,
|
project=name,
|
||||||
use_networking=use_networking,
|
use_networking=use_networking,
|
||||||
networks=networks,
|
networks=service_networks,
|
||||||
links=links,
|
links=links,
|
||||||
net=net,
|
network_mode=network_mode,
|
||||||
volumes_from=volumes_from,
|
volumes_from=volumes_from,
|
||||||
**service_dict))
|
**service_dict)
|
||||||
|
)
|
||||||
project.networks += custom_networks
|
|
||||||
if project.uses_default_network():
|
|
||||||
project.networks.append(project.default_network)
|
|
||||||
|
|
||||||
if config_data.volumes:
|
|
||||||
for vol_name, data in config_data.volumes.items():
|
|
||||||
project.volumes.append(
|
|
||||||
Volume(
|
|
||||||
client=client, project=name, name=vol_name,
|
|
||||||
driver=data.get('driver'),
|
|
||||||
driver_opts=data.get('driver_opts'),
|
|
||||||
external_name=data.get('external_name')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return project
|
return project
|
||||||
|
|
||||||
|
|
@ -188,27 +175,27 @@ class Project(object):
|
||||||
del service_dict['links']
|
del service_dict['links']
|
||||||
return links
|
return links
|
||||||
|
|
||||||
def get_net(self, service_dict):
|
def get_network_mode(self, service_dict, networks):
|
||||||
net = service_dict.pop('net', None)
|
network_mode = service_dict.pop('network_mode', None)
|
||||||
if not net:
|
if not network_mode:
|
||||||
return Net(None)
|
if self.networks.use_networking:
|
||||||
|
return NetworkMode(networks[0]) if networks else NetworkMode('none')
|
||||||
|
return NetworkMode(None)
|
||||||
|
|
||||||
net_name = get_service_name_from_net(net)
|
service_name = get_service_name_from_network_mode(network_mode)
|
||||||
if not net_name:
|
if service_name:
|
||||||
return Net(net)
|
return ServiceNetworkMode(self.get_service(service_name))
|
||||||
|
|
||||||
try:
|
container_name = get_container_name_from_network_mode(network_mode)
|
||||||
return ServiceNet(self.get_service(net_name))
|
if container_name:
|
||||||
except NoSuchService:
|
try:
|
||||||
pass
|
return ContainerNetworkMode(Container.from_id(self.client, container_name))
|
||||||
try:
|
except APIError:
|
||||||
return ContainerNet(Container.from_id(self.client, net_name))
|
raise ConfigurationError(
|
||||||
except APIError:
|
"Service '{name}' uses the network stack of container '{dep}' which "
|
||||||
raise ConfigurationError(
|
"does not exist.".format(name=service_dict['name'], dep=container_name))
|
||||||
'Service "%s" is trying to use the network of "%s", '
|
|
||||||
'which is not the name of a service or container.' % (
|
return NetworkMode(network_mode)
|
||||||
service_dict['name'],
|
|
||||||
net_name))
|
|
||||||
|
|
||||||
def start(self, service_names=None, **options):
|
def start(self, service_names=None, **options):
|
||||||
containers = []
|
containers = []
|
||||||
|
|
@ -236,49 +223,13 @@ class Project(object):
|
||||||
def remove_stopped(self, service_names=None, **options):
|
def remove_stopped(self, service_names=None, **options):
|
||||||
parallel.parallel_remove(self.containers(service_names, stopped=True), options)
|
parallel.parallel_remove(self.containers(service_names, stopped=True), options)
|
||||||
|
|
||||||
def initialize_volumes(self):
|
|
||||||
try:
|
|
||||||
for volume in self.volumes:
|
|
||||||
if volume.external:
|
|
||||||
log.debug(
|
|
||||||
'Volume {0} declared as external. No new '
|
|
||||||
'volume will be created.'.format(volume.name)
|
|
||||||
)
|
|
||||||
if not volume.exists():
|
|
||||||
raise ConfigurationError(
|
|
||||||
'Volume {name} declared as external, but could'
|
|
||||||
' not be found. Please create the volume manually'
|
|
||||||
' using `{command}{name}` and try again.'.format(
|
|
||||||
name=volume.full_name,
|
|
||||||
command='docker volume create --name='
|
|
||||||
)
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
volume.create()
|
|
||||||
except NotFound:
|
|
||||||
raise ConfigurationError(
|
|
||||||
'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 down(self, remove_image_type, include_volumes):
|
def down(self, remove_image_type, include_volumes):
|
||||||
self.stop()
|
self.stop()
|
||||||
self.remove_stopped(v=include_volumes)
|
self.remove_stopped(v=include_volumes)
|
||||||
self.remove_networks()
|
self.networks.remove()
|
||||||
|
|
||||||
if include_volumes:
|
if include_volumes:
|
||||||
self.remove_volumes()
|
self.volumes.remove()
|
||||||
|
|
||||||
self.remove_images(remove_image_type)
|
self.remove_images(remove_image_type)
|
||||||
|
|
||||||
|
|
@ -286,33 +237,6 @@ class Project(object):
|
||||||
for service in self.get_services():
|
for service in self.get_services():
|
||||||
service.remove_image(remove_image_type)
|
service.remove_image(remove_image_type)
|
||||||
|
|
||||||
def remove_networks(self):
|
|
||||||
if not self.use_networking:
|
|
||||||
return
|
|
||||||
for network in self.networks:
|
|
||||||
network.remove()
|
|
||||||
|
|
||||||
def remove_volumes(self):
|
|
||||||
for volume in self.volumes:
|
|
||||||
volume.remove()
|
|
||||||
|
|
||||||
def initialize_networks(self):
|
|
||||||
if not self.use_networking:
|
|
||||||
return
|
|
||||||
|
|
||||||
for network in self.networks:
|
|
||||||
network.ensure()
|
|
||||||
|
|
||||||
def uses_default_network(self):
|
|
||||||
return any(
|
|
||||||
self.default_network.full_name in service.networks
|
|
||||||
for service in self.services
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def default_network(self):
|
|
||||||
return Network(client=self.client, project=self.name, name='default')
|
|
||||||
|
|
||||||
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_restart(containers, options)
|
||||||
|
|
@ -378,13 +302,12 @@ class Project(object):
|
||||||
timeout=DEFAULT_TIMEOUT,
|
timeout=DEFAULT_TIMEOUT,
|
||||||
detached=False):
|
detached=False):
|
||||||
|
|
||||||
services = self.get_services_without_duplicate(service_names, include_deps=start_deps)
|
self.initialize()
|
||||||
|
services = self.get_services_without_duplicate(
|
||||||
|
service_names,
|
||||||
|
include_deps=start_deps)
|
||||||
|
|
||||||
plans = self._get_convergence_plans(services, strategy)
|
plans = self._get_convergence_plans(services, strategy)
|
||||||
|
|
||||||
self.initialize_networks()
|
|
||||||
self.initialize_volumes()
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
container
|
container
|
||||||
for service in services
|
for service in services
|
||||||
|
|
@ -396,6 +319,10 @@ class Project(object):
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
self.networks.initialize()
|
||||||
|
self.volumes.initialize()
|
||||||
|
|
||||||
def _get_convergence_plans(self, services, strategy):
|
def _get_convergence_plans(self, services, strategy):
|
||||||
plans = {}
|
plans = {}
|
||||||
|
|
||||||
|
|
@ -455,22 +382,6 @@ class Project(object):
|
||||||
return acc + dep_services
|
return acc + dep_services
|
||||||
|
|
||||||
|
|
||||||
def get_networks(service_dict, network_definitions):
|
|
||||||
networks = []
|
|
||||||
for name in service_dict.pop('networks', ['default']):
|
|
||||||
if name in ['bridge', 'host']:
|
|
||||||
networks.append(name)
|
|
||||||
else:
|
|
||||||
matches = [n for n in network_definitions if n.name == name]
|
|
||||||
if matches:
|
|
||||||
networks.append(matches[0].full_name)
|
|
||||||
else:
|
|
||||||
raise ConfigurationError(
|
|
||||||
'Service "{}" uses an undefined network "{}"'
|
|
||||||
.format(service_dict['name'], name))
|
|
||||||
return networks
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
if not volumes_from:
|
if not volumes_from:
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,7 @@ from .const import LABEL_SERVICE
|
||||||
from .const import LABEL_VERSION
|
from .const import LABEL_VERSION
|
||||||
from .container import Container
|
from .container import Container
|
||||||
from .parallel import parallel_execute
|
from .parallel import parallel_execute
|
||||||
from .parallel import parallel_remove
|
|
||||||
from .parallel import parallel_start
|
from .parallel import parallel_start
|
||||||
from .parallel import parallel_stop
|
|
||||||
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
|
||||||
|
|
@ -49,7 +47,6 @@ DOCKER_START_KEYS = [
|
||||||
'extra_hosts',
|
'extra_hosts',
|
||||||
'ipc',
|
'ipc',
|
||||||
'read_only',
|
'read_only',
|
||||||
'net',
|
|
||||||
'log_driver',
|
'log_driver',
|
||||||
'log_opt',
|
'log_opt',
|
||||||
'mem_limit',
|
'mem_limit',
|
||||||
|
|
@ -115,7 +112,7 @@ class Service(object):
|
||||||
use_networking=False,
|
use_networking=False,
|
||||||
links=None,
|
links=None,
|
||||||
volumes_from=None,
|
volumes_from=None,
|
||||||
net=None,
|
network_mode=None,
|
||||||
networks=None,
|
networks=None,
|
||||||
**options
|
**options
|
||||||
):
|
):
|
||||||
|
|
@ -125,8 +122,8 @@ class Service(object):
|
||||||
self.use_networking = use_networking
|
self.use_networking = use_networking
|
||||||
self.links = links or []
|
self.links = links or []
|
||||||
self.volumes_from = volumes_from or []
|
self.volumes_from = volumes_from or []
|
||||||
self.net = net or Net(None)
|
self.network_mode = network_mode or NetworkMode(None)
|
||||||
self.networks = networks or []
|
self.networks = networks or {}
|
||||||
self.options = options
|
self.options = options
|
||||||
|
|
||||||
def containers(self, stopped=False, one_off=False, filters={}):
|
def containers(self, stopped=False, one_off=False, filters={}):
|
||||||
|
|
@ -180,6 +177,10 @@ class Service(object):
|
||||||
service.start_container(container)
|
service.start_container(container)
|
||||||
return container
|
return container
|
||||||
|
|
||||||
|
def stop_and_remove(container):
|
||||||
|
container.stop(timeout=timeout)
|
||||||
|
container.remove()
|
||||||
|
|
||||||
running_containers = self.containers(stopped=False)
|
running_containers = self.containers(stopped=False)
|
||||||
num_running = len(running_containers)
|
num_running = len(running_containers)
|
||||||
|
|
||||||
|
|
@ -225,14 +226,17 @@ class Service(object):
|
||||||
|
|
||||||
if desired_num < num_running:
|
if desired_num < num_running:
|
||||||
num_to_stop = num_running - desired_num
|
num_to_stop = num_running - desired_num
|
||||||
|
|
||||||
sorted_running_containers = sorted(
|
sorted_running_containers = sorted(
|
||||||
running_containers,
|
running_containers,
|
||||||
key=attrgetter('number'))
|
key=attrgetter('number'))
|
||||||
parallel_stop(
|
|
||||||
sorted_running_containers[-num_to_stop:],
|
|
||||||
dict(timeout=timeout))
|
|
||||||
|
|
||||||
parallel_remove(self.containers(stopped=True), {})
|
parallel_execute(
|
||||||
|
sorted_running_containers[-num_to_stop:],
|
||||||
|
stop_and_remove,
|
||||||
|
lambda c: c.name,
|
||||||
|
"Stopping and removing",
|
||||||
|
)
|
||||||
|
|
||||||
def create_container(self,
|
def create_container(self,
|
||||||
one_off=False,
|
one_off=False,
|
||||||
|
|
@ -420,16 +424,23 @@ class Service(object):
|
||||||
return self.start_container(container)
|
return self.start_container(container)
|
||||||
|
|
||||||
def start_container(self, container):
|
def start_container(self, container):
|
||||||
container.start()
|
|
||||||
self.connect_container_to_networks(container)
|
self.connect_container_to_networks(container)
|
||||||
|
container.start()
|
||||||
return container
|
return container
|
||||||
|
|
||||||
def connect_container_to_networks(self, container):
|
def connect_container_to_networks(self, container):
|
||||||
for network in self.networks:
|
connected_networks = container.get('NetworkSettings.Networks')
|
||||||
log.debug('Connecting "{}" to "{}"'.format(container.name, network))
|
|
||||||
|
for network, aliases in self.networks.items():
|
||||||
|
if network in connected_networks:
|
||||||
|
self.client.disconnect_container_from_network(
|
||||||
|
container.id, network)
|
||||||
|
|
||||||
self.client.connect_container_to_network(
|
self.client.connect_container_to_network(
|
||||||
container.id, network,
|
container.id, network,
|
||||||
aliases=[self.name])
|
aliases=list(self._get_aliases(container).union(aliases)),
|
||||||
|
links=self._get_links(False),
|
||||||
|
)
|
||||||
|
|
||||||
def remove_duplicate_containers(self, timeout=DEFAULT_TIMEOUT):
|
def remove_duplicate_containers(self, timeout=DEFAULT_TIMEOUT):
|
||||||
for c in self.duplicate_containers():
|
for c in self.duplicate_containers():
|
||||||
|
|
@ -460,7 +471,8 @@ class Service(object):
|
||||||
'options': self.options,
|
'options': self.options,
|
||||||
'image_id': self.image()['Id'],
|
'image_id': self.image()['Id'],
|
||||||
'links': self.get_link_names(),
|
'links': self.get_link_names(),
|
||||||
'net': self.net.id,
|
'net': self.network_mode.id,
|
||||||
|
'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)
|
||||||
|
|
@ -468,10 +480,11 @@ class Service(object):
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_dependency_names(self):
|
def get_dependency_names(self):
|
||||||
net_name = self.net.service_name
|
net_name = self.network_mode.service_name
|
||||||
return (self.get_linked_service_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 []) +
|
||||||
|
self.options.get('depends_on', []))
|
||||||
|
|
||||||
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]
|
||||||
|
|
@ -498,28 +511,38 @@ 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_links(self, link_to_self):
|
def _get_aliases(self, container):
|
||||||
if self.use_networking:
|
if container.labels.get(LABEL_ONE_OFF) == "True":
|
||||||
return []
|
return set()
|
||||||
|
|
||||||
|
return {self.name, container.short_id}
|
||||||
|
|
||||||
|
def _get_links(self, link_to_self):
|
||||||
|
links = {}
|
||||||
|
|
||||||
links = []
|
|
||||||
for service, link_name in self.links:
|
for service, link_name in self.links:
|
||||||
for container in service.containers():
|
for container in service.containers():
|
||||||
links.append((container.name, link_name or service.name))
|
links[link_name or service.name] = container.name
|
||||||
links.append((container.name, container.name))
|
links[container.name] = container.name
|
||||||
links.append((container.name, container.name_without_project))
|
links[container.name_without_project] = container.name
|
||||||
|
|
||||||
if link_to_self:
|
if link_to_self:
|
||||||
for container in self.containers():
|
for container in self.containers():
|
||||||
links.append((container.name, self.name))
|
links[self.name] = container.name
|
||||||
links.append((container.name, container.name))
|
links[container.name] = container.name
|
||||||
links.append((container.name, container.name_without_project))
|
links[container.name_without_project] = container.name
|
||||||
|
|
||||||
for external_link in self.options.get('external_links') or []:
|
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:
|
||||||
external_link, link_name = external_link.split(':')
|
external_link, link_name = external_link.split(':')
|
||||||
links.append((external_link, link_name))
|
links[link_name] = external_link
|
||||||
return links
|
|
||||||
|
return [
|
||||||
|
(alias, container_name)
|
||||||
|
for (container_name, alias) in links.items()
|
||||||
|
]
|
||||||
|
|
||||||
def _get_volumes_from(self):
|
def _get_volumes_from(self):
|
||||||
return [build_volume_from(spec) for spec in self.volumes_from]
|
return [build_volume_from(spec) for spec in self.volumes_from]
|
||||||
|
|
@ -568,20 +591,19 @@ class Service(object):
|
||||||
ports.append(port)
|
ports.append(port)
|
||||||
container_options['ports'] = ports
|
container_options['ports'] = ports
|
||||||
|
|
||||||
override_options['binds'] = merge_volume_bindings(
|
|
||||||
container_options.get('volumes') or [],
|
|
||||||
previous_container)
|
|
||||||
|
|
||||||
if 'volumes' in container_options:
|
|
||||||
container_options['volumes'] = dict(
|
|
||||||
(v.internal, {}) for v in container_options['volumes'])
|
|
||||||
|
|
||||||
container_options['environment'] = merge_environment(
|
container_options['environment'] = merge_environment(
|
||||||
self.options.get('environment'),
|
self.options.get('environment'),
|
||||||
override_options.get('environment'))
|
override_options.get('environment'))
|
||||||
|
|
||||||
if previous_container:
|
binds, affinity = merge_volume_bindings(
|
||||||
container_options['environment']['affinity:container'] = ('=' + previous_container.id)
|
container_options.get('volumes') or [],
|
||||||
|
previous_container)
|
||||||
|
override_options['binds'] = binds
|
||||||
|
container_options['environment'].update(affinity)
|
||||||
|
|
||||||
|
if 'volumes' in container_options:
|
||||||
|
container_options['volumes'] = dict(
|
||||||
|
(v.internal, {}) for v in container_options['volumes'])
|
||||||
|
|
||||||
container_options['image'] = self.image_name
|
container_options['image'] = self.image_name
|
||||||
|
|
||||||
|
|
@ -599,8 +621,8 @@ class Service(object):
|
||||||
override_options,
|
override_options,
|
||||||
one_off=one_off)
|
one_off=one_off)
|
||||||
|
|
||||||
container_options['networking_config'] = self._get_container_networking_config()
|
container_options['environment'] = format_environment(
|
||||||
|
container_options['environment'])
|
||||||
return container_options
|
return container_options
|
||||||
|
|
||||||
def _get_container_host_config(self, override_options, one_off=False):
|
def _get_container_host_config(self, override_options, one_off=False):
|
||||||
|
|
@ -615,7 +637,7 @@ class Service(object):
|
||||||
binds=options.get('binds'),
|
binds=options.get('binds'),
|
||||||
volumes_from=self._get_volumes_from(),
|
volumes_from=self._get_volumes_from(),
|
||||||
privileged=options.get('privileged', False),
|
privileged=options.get('privileged', False),
|
||||||
network_mode=self.net.mode,
|
network_mode=self.network_mode.mode,
|
||||||
devices=options.get('devices'),
|
devices=options.get('devices'),
|
||||||
dns=options.get('dns'),
|
dns=options.get('dns'),
|
||||||
dns_search=options.get('dns_search'),
|
dns_search=options.get('dns_search'),
|
||||||
|
|
@ -635,13 +657,6 @@ class Service(object):
|
||||||
cpu_quota=options.get('cpu_quota'),
|
cpu_quota=options.get('cpu_quota'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_container_networking_config(self):
|
|
||||||
return self.client.create_networking_config({
|
|
||||||
network_name: self.client.create_endpoint_config(aliases=[self.name])
|
|
||||||
for network_name in self.networks
|
|
||||||
if network_name not in ['host', 'bridge']
|
|
||||||
})
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
|
@ -760,22 +775,22 @@ class Service(object):
|
||||||
log.error(six.text_type(e))
|
log.error(six.text_type(e))
|
||||||
|
|
||||||
|
|
||||||
class Net(object):
|
class NetworkMode(object):
|
||||||
"""A `standard` network mode (ex: host, bridge)"""
|
"""A `standard` network mode (ex: host, bridge)"""
|
||||||
|
|
||||||
service_name = None
|
service_name = None
|
||||||
|
|
||||||
def __init__(self, net):
|
def __init__(self, network_mode):
|
||||||
self.net = net
|
self.network_mode = network_mode
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def id(self):
|
def id(self):
|
||||||
return self.net
|
return self.network_mode
|
||||||
|
|
||||||
mode = id
|
mode = id
|
||||||
|
|
||||||
|
|
||||||
class ContainerNet(object):
|
class ContainerNetworkMode(object):
|
||||||
"""A network mode that uses a container's network stack."""
|
"""A network mode that uses a container's network stack."""
|
||||||
|
|
||||||
service_name = None
|
service_name = None
|
||||||
|
|
@ -792,7 +807,7 @@ class ContainerNet(object):
|
||||||
return 'container:' + self.container.id
|
return 'container:' + self.container.id
|
||||||
|
|
||||||
|
|
||||||
class ServiceNet(object):
|
class ServiceNetworkMode(object):
|
||||||
"""A network mode that uses a service's network stack."""
|
"""A network mode that uses a service's network stack."""
|
||||||
|
|
||||||
def __init__(self, service):
|
def __init__(self, service):
|
||||||
|
|
@ -861,18 +876,23 @@ def merge_volume_bindings(volumes, previous_container):
|
||||||
"""Return a list of volume bindings for a container. Container data volumes
|
"""Return a list of volume bindings for a container. Container data volumes
|
||||||
are replaced by those from the previous container.
|
are replaced by those from the previous container.
|
||||||
"""
|
"""
|
||||||
|
affinity = {}
|
||||||
|
|
||||||
volume_bindings = dict(
|
volume_bindings = dict(
|
||||||
build_volume_binding(volume)
|
build_volume_binding(volume)
|
||||||
for volume in volumes
|
for volume in volumes
|
||||||
if volume.external)
|
if volume.external)
|
||||||
|
|
||||||
if previous_container:
|
if previous_container:
|
||||||
data_volumes = get_container_data_volumes(previous_container, volumes)
|
old_volumes = get_container_data_volumes(previous_container, volumes)
|
||||||
warn_on_masked_volume(volumes, data_volumes, previous_container.service)
|
warn_on_masked_volume(volumes, old_volumes, previous_container.service)
|
||||||
volume_bindings.update(
|
volume_bindings.update(
|
||||||
build_volume_binding(volume) for volume in data_volumes)
|
build_volume_binding(volume) for volume in old_volumes)
|
||||||
|
|
||||||
return list(volume_bindings.values())
|
if old_volumes:
|
||||||
|
affinity = {'affinity:container': '=' + previous_container.id}
|
||||||
|
|
||||||
|
return list(volume_bindings.values()), affinity
|
||||||
|
|
||||||
|
|
||||||
def get_container_data_volumes(container, volumes_option):
|
def get_container_data_volumes(container, volumes_option):
|
||||||
|
|
@ -904,8 +924,12 @@ def get_container_data_volumes(container, volumes_option):
|
||||||
if not mount:
|
if not mount:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Volume was previously a host volume, now it's a container volume
|
||||||
|
if not mount.get('Name'):
|
||||||
|
continue
|
||||||
|
|
||||||
# Copy existing volume from old container
|
# Copy existing volume from old container
|
||||||
volume = volume._replace(external=mount['Source'])
|
volume = volume._replace(external=mount['Name'])
|
||||||
volumes.append(volume)
|
volumes.append(volume)
|
||||||
|
|
||||||
return volumes
|
return volumes
|
||||||
|
|
@ -996,3 +1020,12 @@ def get_log_config(logging_dict):
|
||||||
type=log_driver,
|
type=log_driver,
|
||||||
config=log_options
|
config=log_options
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: remove once fix is available in docker-py
|
||||||
|
def format_environment(environment):
|
||||||
|
def format_env(key, value):
|
||||||
|
if value is None:
|
||||||
|
return key
|
||||||
|
return '{key}={value}'.format(key=key, value=value)
|
||||||
|
return [format_env(*item) for item in environment.items()]
|
||||||
|
|
|
||||||
|
|
@ -92,3 +92,7 @@ def json_hash(obj):
|
||||||
|
|
||||||
def microseconds_from_time_nano(time_nano):
|
def microseconds_from_time_nano(time_nano):
|
||||||
return int(time_nano % 1000000000 / 1000)
|
return int(time_nano % 1000000000 / 1000)
|
||||||
|
|
||||||
|
|
||||||
|
def build_string_dict(source_dict):
|
||||||
|
return dict((k, str(v)) for k, v in source_dict.items())
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@ 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 .config import ConfigurationError
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -50,3 +52,77 @@ class Volume(object):
|
||||||
if self.external_name:
|
if self.external_name:
|
||||||
return self.external_name
|
return self.external_name
|
||||||
return '{0}_{1}'.format(self.project, self.name)
|
return '{0}_{1}'.format(self.project, self.name)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectVolumes(object):
|
||||||
|
|
||||||
|
def __init__(self, volumes):
|
||||||
|
self.volumes = volumes
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_config(cls, name, config_data, client):
|
||||||
|
config_volumes = config_data.volumes or {}
|
||||||
|
volumes = {
|
||||||
|
vol_name: Volume(
|
||||||
|
client=client,
|
||||||
|
project=name,
|
||||||
|
name=vol_name,
|
||||||
|
driver=data.get('driver'),
|
||||||
|
driver_opts=data.get('driver_opts'),
|
||||||
|
external_name=data.get('external_name')
|
||||||
|
)
|
||||||
|
for vol_name, data in config_volumes.items()
|
||||||
|
}
|
||||||
|
return cls(volumes)
|
||||||
|
|
||||||
|
def remove(self):
|
||||||
|
for volume in self.volumes.values():
|
||||||
|
volume.remove()
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
try:
|
||||||
|
for volume in self.volumes.values():
|
||||||
|
if volume.external:
|
||||||
|
log.debug(
|
||||||
|
'Volume {0} declared as external. No new '
|
||||||
|
'volume will be created.'.format(volume.name)
|
||||||
|
)
|
||||||
|
if not volume.exists():
|
||||||
|
raise ConfigurationError(
|
||||||
|
'Volume {name} declared as external, but could'
|
||||||
|
' not be found. Please create the volume manually'
|
||||||
|
' using `{command}{name}` and try again.'.format(
|
||||||
|
name=volume.full_name,
|
||||||
|
command='docker volume create --name='
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
log.info(
|
||||||
|
'Creating volume "{0}" with {1} driver'.format(
|
||||||
|
volume.full_name, volume.driver or 'default'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
volume.create()
|
||||||
|
except NotFound:
|
||||||
|
raise ConfigurationError(
|
||||||
|
'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):
|
||||||
|
if not volume_spec.is_named_volume:
|
||||||
|
return volume_spec
|
||||||
|
|
||||||
|
volume = self.volumes[volume_spec.external]
|
||||||
|
return volume_spec._replace(external=volume.full_name)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,10 @@
|
||||||
# . ~/.docker-compose-completion.sh
|
# . ~/.docker-compose-completion.sh
|
||||||
|
|
||||||
|
|
||||||
|
__docker_compose_q() {
|
||||||
|
docker-compose 2>/dev/null ${compose_file:+-f $compose_file} ${compose_project:+-p $compose_project} "$@"
|
||||||
|
}
|
||||||
|
|
||||||
# suppress trailing whitespace
|
# suppress trailing whitespace
|
||||||
__docker_compose_nospace() {
|
__docker_compose_nospace() {
|
||||||
# compopt is not available in ancient bash versions
|
# compopt is not available in ancient bash versions
|
||||||
|
|
@ -39,7 +43,7 @@ __docker_compose_compose_file() {
|
||||||
|
|
||||||
# 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() {
|
||||||
awk -F: '/^[a-zA-Z0-9]/{print $1}' "${compose_file:-$(__docker_compose_compose_file)}" 2>/dev/null
|
__docker_compose_q config --services
|
||||||
}
|
}
|
||||||
|
|
||||||
# All services, even those without an existing container
|
# All services, even those without an existing container
|
||||||
|
|
@ -49,8 +53,12 @@ __docker_compose_services_all() {
|
||||||
|
|
||||||
# All services that have an entry with the given key in their compose_file section
|
# All services that have an entry with the given key in their compose_file section
|
||||||
___docker_compose_services_with_key() {
|
___docker_compose_services_with_key() {
|
||||||
# flatten sections to one line, then filter lines containing the key and return section name.
|
# flatten sections under "services" to one line, then filter lines containing the key and return section name
|
||||||
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}'
|
__docker_compose_q config \
|
||||||
|
| sed -n -e '/^services:/,/^[^ ]/p' \
|
||||||
|
| sed -n 's/^ //p' \
|
||||||
|
| awk '/^[a-zA-Z0-9]/{printf "\n"};{printf $0;next;}' \
|
||||||
|
| awk -F: -v key=": +$1:" '$0 ~ key {print $1}'
|
||||||
}
|
}
|
||||||
|
|
||||||
# All services that are defined by a Dockerfile reference
|
# All services that are defined by a Dockerfile reference
|
||||||
|
|
@ -67,11 +75,9 @@ __docker_compose_services_from_image() {
|
||||||
# by a boolean expression passed in as argument.
|
# by a boolean expression passed in as argument.
|
||||||
__docker_compose_services_with() {
|
__docker_compose_services_with() {
|
||||||
local containers names
|
local containers names
|
||||||
containers="$(docker-compose 2>/dev/null ${compose_file:+-f $compose_file} ${compose_project:+-p $compose_project} ps -q)"
|
containers="$(__docker_compose_q ps -q)"
|
||||||
names=( $(docker 2>/dev/null inspect --format "{{if ${1:-true}}} {{ .Name }} {{end}}" $containers) )
|
names=$(docker 2>/dev/null inspect -f "{{if ${1:-true}}}{{range \$k, \$v := .Config.Labels}}{{if eq \$k \"com.docker.compose.service\"}}{{\$v}}{{end}}{{end}}{{end}}" $containers)
|
||||||
names=( ${names[@]%_*} ) # strip trailing numbers
|
COMPREPLY=( $(compgen -W "$names" -- "$cur") )
|
||||||
names=( ${names[@]#*_} ) # strip project name
|
|
||||||
COMPREPLY=( $(compgen -W "${names[*]}" -- "$cur") )
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# The services for which at least one paused container exists
|
# The services for which at least one paused container exists
|
||||||
|
|
@ -107,6 +113,18 @@ _docker_compose_config() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_docker_compose_create() {
|
||||||
|
case "$cur" in
|
||||||
|
-*)
|
||||||
|
COMPREPLY=( $( compgen -W "--force-recreate --help --no-build --no-recreate" -- "$cur" ) )
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
__docker_compose_services_all
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
_docker_compose_docker_compose() {
|
_docker_compose_docker_compose() {
|
||||||
case "$prev" in
|
case "$prev" in
|
||||||
--file|-f)
|
--file|-f)
|
||||||
|
|
@ -409,6 +427,7 @@ _docker_compose() {
|
||||||
local commands=(
|
local commands=(
|
||||||
build
|
build
|
||||||
config
|
config
|
||||||
|
create
|
||||||
down
|
down
|
||||||
events
|
events
|
||||||
help
|
help
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,7 @@ __docker-compose_commands() {
|
||||||
local -a lines
|
local -a lines
|
||||||
lines=(${(f)"$(_call_program commands docker-compose 2>&1)"})
|
lines=(${(f)"$(_call_program commands docker-compose 2>&1)"})
|
||||||
_docker_compose_subcommands=(${${${lines[$((${lines[(i)Commands:]} + 1)),${lines[(I) *]}]}## #}/ ##/:})
|
_docker_compose_subcommands=(${${${lines[$((${lines[(i)Commands:]} + 1)),${lines[(I) *]}]}## #}/ ##/:})
|
||||||
_store_cache docker_compose_subcommands _docker_compose_subcommands
|
(( $#_docker_compose_subcommands > 0 )) && _store_cache docker_compose_subcommands _docker_compose_subcommands
|
||||||
fi
|
fi
|
||||||
_describe -t docker-compose-commands "docker-compose command" _docker_compose_subcommands
|
_describe -t docker-compose-commands "docker-compose command" _docker_compose_subcommands
|
||||||
}
|
}
|
||||||
|
|
@ -203,6 +203,20 @@ __docker-compose_subcommand() {
|
||||||
'(--quiet -q)'{--quiet,-q}"[Only validate the configuration, don't print anything.]" \
|
'(--quiet -q)'{--quiet,-q}"[Only validate the configuration, don't print anything.]" \
|
||||||
'--services[Print the service names, one per line.]' && ret=0
|
'--services[Print the service names, one per line.]' && ret=0
|
||||||
;;
|
;;
|
||||||
|
(create)
|
||||||
|
_arguments \
|
||||||
|
$opts_help \
|
||||||
|
"(--no-recreate --no-build)--force-recreate[Recreate containers even if their configuration and image haven't changed. Incompatible with --no-recreate.]" \
|
||||||
|
"(--force-recreate)--no-build[If containers already exist, don't recreate them. Incompatible with --force-recreate.]" \
|
||||||
|
"(--force-recreate)--no-recreate[Don't build an image, even if it's missing]" \
|
||||||
|
'*:services:__docker-compose_services_all' && ret=0
|
||||||
|
;;
|
||||||
|
(down)
|
||||||
|
_arguments \
|
||||||
|
$opts_help \
|
||||||
|
"--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 data volumes]" && ret=0
|
||||||
|
;;
|
||||||
(events)
|
(events)
|
||||||
_arguments \
|
_arguments \
|
||||||
$opts_help \
|
$opts_help \
|
||||||
|
|
@ -298,12 +312,13 @@ __docker-compose_subcommand() {
|
||||||
(up)
|
(up)
|
||||||
_arguments \
|
_arguments \
|
||||||
$opts_help \
|
$opts_help \
|
||||||
'-d[Detached mode: Run containers in the background, print new container names.]' \
|
'(--abort-on-container-exit)-d[Detached mode: Run containers in the background, print new container names.]' \
|
||||||
'--no-color[Produce monochrome output.]' \
|
'--no-color[Produce monochrome output.]' \
|
||||||
"--no-deps[Don't start linked services.]" \
|
"--no-deps[Don't start linked services.]" \
|
||||||
"--force-recreate[Recreate containers even if their configuration and image haven't changed. Incompatible with --no-recreate.]" \
|
"--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.]" \
|
"--no-recreate[If containers already exist, don't recreate them.]" \
|
||||||
"--no-build[Don't build an image, even if it's missing]" \
|
"--no-build[Don't build an image, even if it's missing]" \
|
||||||
|
"(-d)--abort-on-container-exit[Stops all containers if any container was stopped. Incompatible with -d.]" \
|
||||||
'(-t --timeout)'{-t,--timeout}"[Specify a shutdown timeout in seconds. (default: 10)]:seconds: " \
|
'(-t --timeout)'{-t,--timeout}"[Specify a shutdown timeout in seconds. (default: 10)]:seconds: " \
|
||||||
'*:services:__docker-compose_services_all' && ret=0
|
'*:services:__docker-compose_services_all' && ret=0
|
||||||
;;
|
;;
|
||||||
|
|
|
||||||
173
contrib/migration/migrate-compose-file-v1-to-v2.py
Executable file
173
contrib/migration/migrate-compose-file-v1-to-v2.py
Executable file
|
|
@ -0,0 +1,173 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""
|
||||||
|
Migrate a Compose file from the V1 format in Compose 1.5 to the V2 format
|
||||||
|
supported by Compose 1.6+
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import ruamel.yaml
|
||||||
|
|
||||||
|
from compose.config.types import VolumeSpec
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger('migrate')
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(content):
|
||||||
|
data = ruamel.yaml.load(content, ruamel.yaml.RoundTripLoader)
|
||||||
|
|
||||||
|
service_names = data.keys()
|
||||||
|
|
||||||
|
for name, service in data.items():
|
||||||
|
warn_for_links(name, service)
|
||||||
|
warn_for_external_links(name, service)
|
||||||
|
rewrite_net(service, service_names)
|
||||||
|
rewrite_build(service)
|
||||||
|
rewrite_logging(service)
|
||||||
|
rewrite_volumes_from(service, service_names)
|
||||||
|
|
||||||
|
services = {name: data.pop(name) for name in data.keys()}
|
||||||
|
|
||||||
|
data['version'] = "2"
|
||||||
|
data['services'] = services
|
||||||
|
create_volumes_section(data)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def warn_for_links(name, service):
|
||||||
|
links = service.get('links')
|
||||||
|
if links:
|
||||||
|
example_service = links[0].partition(':')[0]
|
||||||
|
log.warn(
|
||||||
|
"Service {name} has links, which no longer create environment "
|
||||||
|
"variables such as {example_service_upper}_PORT. "
|
||||||
|
"If you are using those in your application code, you should "
|
||||||
|
"instead connect directly to the hostname, e.g. "
|
||||||
|
"'{example_service}'."
|
||||||
|
.format(name=name, example_service=example_service,
|
||||||
|
example_service_upper=example_service.upper()))
|
||||||
|
|
||||||
|
|
||||||
|
def warn_for_external_links(name, service):
|
||||||
|
external_links = service.get('external_links')
|
||||||
|
if external_links:
|
||||||
|
log.warn(
|
||||||
|
"Service {name} has external_links: {ext}, which now work "
|
||||||
|
"slightly differently. In particular, two containers must be "
|
||||||
|
"connected to at least one network in common in order to "
|
||||||
|
"communicate, even if explicitly linked together.\n\n"
|
||||||
|
"Either connect the external container to your app's default "
|
||||||
|
"network, or connect both the external container and your "
|
||||||
|
"service's containers to a pre-existing network. See "
|
||||||
|
"https://docs.docker.com/compose/networking/ "
|
||||||
|
"for more on how to do this."
|
||||||
|
.format(name=name, ext=external_links))
|
||||||
|
|
||||||
|
|
||||||
|
def rewrite_net(service, service_names):
|
||||||
|
if 'net' in service:
|
||||||
|
network_mode = service.pop('net')
|
||||||
|
|
||||||
|
# "container:<service name>" is now "service:<service name>"
|
||||||
|
if network_mode.startswith('container:'):
|
||||||
|
name = network_mode.partition(':')[2]
|
||||||
|
if name in service_names:
|
||||||
|
network_mode = 'service:{}'.format(name)
|
||||||
|
|
||||||
|
service['network_mode'] = network_mode
|
||||||
|
|
||||||
|
|
||||||
|
def rewrite_build(service):
|
||||||
|
if 'dockerfile' in service:
|
||||||
|
service['build'] = {
|
||||||
|
'context': service.pop('build'),
|
||||||
|
'dockerfile': service.pop('dockerfile'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def rewrite_logging(service):
|
||||||
|
if 'log_driver' in service:
|
||||||
|
service['logging'] = {'driver': service.pop('log_driver')}
|
||||||
|
if 'log_opt' in service:
|
||||||
|
service['logging']['options'] = service.pop('log_opt')
|
||||||
|
|
||||||
|
|
||||||
|
def rewrite_volumes_from(service, service_names):
|
||||||
|
for idx, volume_from in enumerate(service.get('volumes_from', [])):
|
||||||
|
if volume_from.split(':', 1)[0] not in service_names:
|
||||||
|
service['volumes_from'][idx] = 'container:%s' % volume_from
|
||||||
|
|
||||||
|
|
||||||
|
def create_volumes_section(data):
|
||||||
|
named_volumes = get_named_volumes(data['services'])
|
||||||
|
if named_volumes:
|
||||||
|
log.warn(
|
||||||
|
"Named volumes ({names}) must be explicitly declared. Creating a "
|
||||||
|
"'volumes' section with declarations.\n\n"
|
||||||
|
"For backwards-compatibility, they've been declared as external. "
|
||||||
|
"If you don't mind the volume names being prefixed with the "
|
||||||
|
"project name, you can remove the 'external' option from each one."
|
||||||
|
.format(names=', '.join(list(named_volumes))))
|
||||||
|
|
||||||
|
data['volumes'] = named_volumes
|
||||||
|
|
||||||
|
|
||||||
|
def get_named_volumes(services):
|
||||||
|
volume_specs = [
|
||||||
|
VolumeSpec.parse(volume)
|
||||||
|
for service in services.values()
|
||||||
|
for volume in service.get('volumes', [])
|
||||||
|
]
|
||||||
|
names = {
|
||||||
|
spec.external
|
||||||
|
for spec in volume_specs
|
||||||
|
if spec.is_named_volume
|
||||||
|
}
|
||||||
|
return {name: {'external': True} for name in names}
|
||||||
|
|
||||||
|
|
||||||
|
def write(stream, new_format, indent, width):
|
||||||
|
ruamel.yaml.dump(
|
||||||
|
new_format,
|
||||||
|
stream,
|
||||||
|
Dumper=ruamel.yaml.RoundTripDumper,
|
||||||
|
indent=indent,
|
||||||
|
width=width)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_opts(args):
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("filename", help="Compose file filename.")
|
||||||
|
parser.add_argument("-i", "--in-place", action='store_true')
|
||||||
|
parser.add_argument(
|
||||||
|
"--indent", type=int, default=2,
|
||||||
|
help="Number of spaces used to indent the output yaml.")
|
||||||
|
parser.add_argument(
|
||||||
|
"--width", type=int, default=80,
|
||||||
|
help="Number of spaces used as the output width.")
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
logging.basicConfig(format='\033[33m%(levelname)s:\033[37m %(message)s\033[0m\n')
|
||||||
|
|
||||||
|
opts = parse_opts(args)
|
||||||
|
|
||||||
|
with open(opts.filename, 'r') as fh:
|
||||||
|
new_format = migrate(fh.read())
|
||||||
|
|
||||||
|
if opts.in_place:
|
||||||
|
output = open(opts.filename, 'w')
|
||||||
|
else:
|
||||||
|
output = sys.stdout
|
||||||
|
write(output, new_format, opts.indent, opts.width)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main(sys.argv)
|
||||||
|
|
@ -23,8 +23,8 @@ exe = EXE(pyz,
|
||||||
'DATA'
|
'DATA'
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'compose/config/fields_schema_v2.json',
|
'compose/config/fields_schema_v2.0.json',
|
||||||
'compose/config/fields_schema_v2.json',
|
'compose/config/fields_schema_v2.0.json',
|
||||||
'DATA'
|
'DATA'
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
|
@ -33,8 +33,8 @@ exe = EXE(pyz,
|
||||||
'DATA'
|
'DATA'
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'compose/config/service_schema_v2.json',
|
'compose/config/service_schema_v2.0.json',
|
||||||
'compose/config/service_schema_v2.json',
|
'compose/config/service_schema_v2.0.json',
|
||||||
'DATA'
|
'DATA'
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,10 @@ RUN svn checkout https://github.com/docker/docker/trunk/docs /docs/content/engin
|
||||||
RUN svn checkout https://github.com/docker/swarm/trunk/docs /docs/content/swarm
|
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/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/distribution/trunk/docs /docs/content/registry
|
||||||
RUN svn checkout https://github.com/kitematic/kitematic/trunk/docs /docs/content/kitematic
|
RUN svn checkout https://github.com/docker/notary/trunk/docs /docs/content/notary
|
||||||
RUN svn checkout https://github.com/docker/tutorials/trunk/docs /docs/content/tutorials
|
RUN svn checkout https://github.com/docker/kitematic/trunk/docs /docs/content/kitematic
|
||||||
RUN svn checkout https://github.com/docker/opensource/trunk/docs /docs/content
|
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
|
ENV PROJECT=compose
|
||||||
# To get the git info for this repo
|
# To get the git info for this repo
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ The top of each Docker Compose documentation file contains TOML metadata. The me
|
||||||
description = "How to use Docker Compose's extends keyword to share configuration between files and projects"
|
description = "How to use Docker Compose's extends keyword to share configuration between files and projects"
|
||||||
keywords = ["fig, composition, compose, docker, orchestration, documentation, docs"]
|
keywords = ["fig, composition, compose, docker, orchestration, documentation, docs"]
|
||||||
[menu.main]
|
[menu.main]
|
||||||
parent="smn_workw_compose"
|
parent="workw_compose"
|
||||||
weight=2
|
weight=2
|
||||||
+++
|
+++
|
||||||
<![end-metadata]-->
|
<![end-metadata]-->
|
||||||
|
|
@ -70,7 +70,7 @@ The metadata alone has this structure:
|
||||||
description = "How to use Docker Compose's extends keyword to share configuration between files and projects"
|
description = "How to use Docker Compose's extends keyword to share configuration between files and projects"
|
||||||
keywords = ["fig, composition, compose, docker, orchestration, documentation, docs"]
|
keywords = ["fig, composition, compose, docker, orchestration, documentation, docs"]
|
||||||
[menu.main]
|
[menu.main]
|
||||||
parent="smn_workw_compose"
|
parent="workw_compose"
|
||||||
weight=2
|
weight=2
|
||||||
+++
|
+++
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ title = "Command-line Completion"
|
||||||
description = "Compose CLI reference"
|
description = "Compose CLI reference"
|
||||||
keywords = ["fig, composition, compose, docker, orchestration, cli, reference"]
|
keywords = ["fig, composition, compose, docker, orchestration, cli, reference"]
|
||||||
[menu.main]
|
[menu.main]
|
||||||
parent="smn_workw_compose"
|
parent="workw_compose"
|
||||||
weight=10
|
weight=88
|
||||||
+++
|
+++
|
||||||
<![end-metadata]-->
|
<![end-metadata]-->
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,90 +1,45 @@
|
||||||
<!--[metadata]>
|
<!--[metadata]>
|
||||||
+++
|
+++
|
||||||
title = "Compose file reference"
|
title = "Compose File Reference"
|
||||||
description = "Compose file reference"
|
description = "Compose file reference"
|
||||||
keywords = ["fig, composition, compose, docker"]
|
keywords = ["fig, composition, compose, docker"]
|
||||||
aliases = ["/compose/yml"]
|
aliases = ["/compose/yml"]
|
||||||
[menu.main]
|
[menu.main]
|
||||||
parent="smn_compose_ref"
|
parent="workw_compose"
|
||||||
|
weight=70
|
||||||
+++
|
+++
|
||||||
<![end-metadata]-->
|
<![end-metadata]-->
|
||||||
|
|
||||||
|
|
||||||
# Compose file reference
|
# Compose file reference
|
||||||
|
|
||||||
The compose file is a [YAML](http://yaml.org/) file where all the top level
|
The Compose file is a [YAML](http://yaml.org/) file defining
|
||||||
keys are the name of a service, and the values are the service definition.
|
[services](#service-configuration-reference),
|
||||||
The default path for a compose file is `./docker-compose.yml`.
|
[networks](#network-configuration-reference) and
|
||||||
|
[volumes](#volume-configuration-reference).
|
||||||
|
The default path for a Compose file is `./docker-compose.yml`.
|
||||||
|
|
||||||
Each service defined in `docker-compose.yml` must specify exactly one of
|
A service definition contains configuration which will be applied to each
|
||||||
`image` or `build`. Other keys are optional, and are analogous to their
|
container started for that service, much like passing command-line parameters to
|
||||||
`docker run` command-line counterparts.
|
`docker run`. Likewise, network and volume definitions are analogous to
|
||||||
|
`docker network create` and `docker volume create`.
|
||||||
|
|
||||||
As with `docker run`, options specified in the Dockerfile (e.g., `CMD`,
|
As with `docker run`, options specified in the Dockerfile (e.g., `CMD`,
|
||||||
`EXPOSE`, `VOLUME`, `ENV`) are respected by default - you don't need to
|
`EXPOSE`, `VOLUME`, `ENV`) are respected by default - you don't need to
|
||||||
specify them again in `docker-compose.yml`.
|
specify them again in `docker-compose.yml`.
|
||||||
|
|
||||||
## Versioning
|
You can use environment variables in configuration values with a Bash-like
|
||||||
|
`${VARIABLE}` syntax - see [variable substitution](#variable-substitution) for
|
||||||
It is possible to use different versions of the `compose.yml` format.
|
full details.
|
||||||
Below are the formats currently supported by compose.
|
|
||||||
|
|
||||||
|
|
||||||
### Version 1
|
|
||||||
|
|
||||||
Compose files that do not declare a version are considered "version 1". In
|
|
||||||
those files, all the [services](#service-configuration-reference) are declared
|
|
||||||
at the root of the document.
|
|
||||||
|
|
||||||
Version 1 files do not support the declaration of
|
|
||||||
named [volumes](#volume-configuration-reference) or
|
|
||||||
[build arguments](#args).
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
web:
|
|
||||||
build: .
|
|
||||||
ports:
|
|
||||||
- "5000:5000"
|
|
||||||
volumes:
|
|
||||||
- .:/code
|
|
||||||
- logvolume01:/var/log
|
|
||||||
links:
|
|
||||||
- redis
|
|
||||||
redis:
|
|
||||||
image: redis
|
|
||||||
|
|
||||||
|
|
||||||
### Version 2
|
|
||||||
|
|
||||||
Compose files using the version 2 syntax must indicate the version number at
|
|
||||||
the root of the document. All [services](#service-configuration-reference)
|
|
||||||
must be declared under the `services` key.
|
|
||||||
Named [volumes](#volume-configuration-reference) must be declared under the
|
|
||||||
`volumes` key.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
version: 2
|
|
||||||
services:
|
|
||||||
web:
|
|
||||||
build: .
|
|
||||||
ports:
|
|
||||||
- "5000:5000"
|
|
||||||
volumes:
|
|
||||||
- .:/code
|
|
||||||
- logvolume01:/var/log
|
|
||||||
links:
|
|
||||||
- redis
|
|
||||||
redis:
|
|
||||||
image: redis
|
|
||||||
volumes:
|
|
||||||
logvolume01:
|
|
||||||
driver: default
|
|
||||||
|
|
||||||
|
|
||||||
## Service configuration reference
|
## Service configuration reference
|
||||||
|
|
||||||
|
> **Note:** There are two versions of the Compose file format – version 1 (the
|
||||||
|
> legacy format, which does not support volumes or networks) and version 2 (the
|
||||||
|
> most up-to-date). For more information, see the [Versioning](#versioning)
|
||||||
|
> section.
|
||||||
|
|
||||||
This section contains a list of all configuration options supported by a service
|
This section contains a list of all configuration options supported by a service
|
||||||
definition.
|
definition.
|
||||||
|
|
||||||
|
|
@ -92,28 +47,30 @@ definition.
|
||||||
|
|
||||||
Configuration options that are applied at build time.
|
Configuration options that are applied at build time.
|
||||||
|
|
||||||
In version 1 this must be given as a string representing the context.
|
`build` can be specified either as a string containing a path to the build
|
||||||
|
context, or an object with the path specified under [context](#context) and
|
||||||
|
optionally [dockerfile](#dockerfile) and [args](#args).
|
||||||
|
|
||||||
build: .
|
build: ./dir
|
||||||
|
|
||||||
In version 2 this can alternatively be given as an object with extra options.
|
build:
|
||||||
|
context: ./dir
|
||||||
|
dockerfile: Dockerfile-alternate
|
||||||
|
args:
|
||||||
|
buildno: 1
|
||||||
|
|
||||||
version: 2
|
> **Note**: In the [version 1 file format](#version-1), `build` is different in
|
||||||
services:
|
> two ways:
|
||||||
web:
|
>
|
||||||
build: .
|
> - Only the string form (`build: .`) is allowed - not the object form.
|
||||||
|
> - Using `build` together with `image` is not allowed. Attempting to do so
|
||||||
version: 2
|
> results in an error.
|
||||||
services:
|
|
||||||
web:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile-alternate
|
|
||||||
args:
|
|
||||||
buildno: 1
|
|
||||||
|
|
||||||
#### context
|
#### context
|
||||||
|
|
||||||
|
> [Version 2 file format](#version-2) only. In version 1, just use
|
||||||
|
> [build](#build).
|
||||||
|
|
||||||
Either a path to a directory containing a Dockerfile, or a url to a git repository.
|
Either a path to a directory containing a Dockerfile, or a url to a git repository.
|
||||||
|
|
||||||
When the value supplied is a relative path, it is interpreted as relative to the
|
When the value supplied is a relative path, it is interpreted as relative to the
|
||||||
|
|
@ -122,29 +79,34 @@ sent to the Docker daemon.
|
||||||
|
|
||||||
Compose will build and tag it with a generated name, and use that image thereafter.
|
Compose will build and tag it with a generated name, and use that image thereafter.
|
||||||
|
|
||||||
build: /path/to/build/dir
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
context: /path/to/build/dir
|
context: ./dir
|
||||||
|
|
||||||
Using `context` together with `image` is not allowed. Attempting to do so results in
|
|
||||||
an error.
|
|
||||||
|
|
||||||
#### dockerfile
|
#### dockerfile
|
||||||
|
|
||||||
Alternate Dockerfile.
|
Alternate Dockerfile.
|
||||||
|
|
||||||
Compose will use an alternate file to build with. A build path must also be
|
Compose will use an alternate file to build with. A build path must also be
|
||||||
specified using the `build` key.
|
specified.
|
||||||
|
|
||||||
build:
|
build:
|
||||||
context: /path/to/build/dir
|
context: .
|
||||||
dockerfile: Dockerfile-alternate
|
dockerfile: Dockerfile-alternate
|
||||||
|
|
||||||
Using `dockerfile` together with `image` is not allowed. Attempting to do so results in an error.
|
> **Note**: In the [version 1 file format](#version-1), `dockerfile` is
|
||||||
|
> different in two ways:
|
||||||
|
>
|
||||||
|
> - It appears alongside `build`, not as a sub-option:
|
||||||
|
>
|
||||||
|
> build: .
|
||||||
|
> dockerfile: Dockerfile-alternate
|
||||||
|
> - Using `dockerfile` together with `image` is not allowed. Attempting to do
|
||||||
|
> so results in an error.
|
||||||
|
|
||||||
#### args
|
#### args
|
||||||
|
|
||||||
|
> [Version 2 file format](#version-2) only.
|
||||||
|
|
||||||
Add build arguments. You can use either an array or a dictionary. Any
|
Add build arguments. You can use either an array or a dictionary. Any
|
||||||
boolean values; true, false, yes, no, need to be enclosed in quotes to ensure
|
boolean values; true, false, yes, no, need to be enclosed in quotes to ensure
|
||||||
they are not converted to True or False by the YML parser.
|
they are not converted to True or False by the YML parser.
|
||||||
|
|
@ -152,8 +114,6 @@ they are not converted to True or False by the YML parser.
|
||||||
Build arguments with only a key are resolved to their environment value on the
|
Build arguments with only a key are resolved to their environment value on the
|
||||||
machine Compose is running on.
|
machine Compose is running on.
|
||||||
|
|
||||||
> **Note:** Introduced in version 2 of the compose file format.
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
args:
|
args:
|
||||||
buildno: 1
|
buildno: 1
|
||||||
|
|
@ -210,6 +170,31 @@ client create option.
|
||||||
devices:
|
devices:
|
||||||
- "/dev/ttyUSB0:/dev/ttyUSB0"
|
- "/dev/ttyUSB0:/dev/ttyUSB0"
|
||||||
|
|
||||||
|
### depends_on
|
||||||
|
|
||||||
|
Express dependency between services, which has two effects:
|
||||||
|
|
||||||
|
- `docker-compose up` will start services in dependency order. In the following
|
||||||
|
example, `db` and `redis` will be started before `web`.
|
||||||
|
|
||||||
|
- `docker-compose up SERVICE` will automatically include `SERVICE`'s
|
||||||
|
dependencies. In the following example, `docker-compose up web` will also
|
||||||
|
create and start `db` and `redis`.
|
||||||
|
|
||||||
|
Simple example:
|
||||||
|
|
||||||
|
version: '2'
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- redis
|
||||||
|
redis:
|
||||||
|
image: redis
|
||||||
|
db:
|
||||||
|
image: postgres
|
||||||
|
|
||||||
### dns
|
### dns
|
||||||
|
|
||||||
Custom DNS servers. Can be a single value or a list.
|
Custom DNS servers. Can be a single value or a list.
|
||||||
|
|
@ -336,6 +321,10 @@ container name and the link alias (`CONTAINER:ALIAS`).
|
||||||
- project_db_1:mysql
|
- project_db_1:mysql
|
||||||
- project_db_1:postgresql
|
- project_db_1:postgresql
|
||||||
|
|
||||||
|
> **Note:** If you're using the [version 2 file format](#version-2), the
|
||||||
|
> externally-created containers must be connected to at least one of the same
|
||||||
|
> networks as the service which is linking to them.
|
||||||
|
|
||||||
### extra_hosts
|
### extra_hosts
|
||||||
|
|
||||||
Add hostname mappings. Use the same values as the docker client `--add-host` parameter.
|
Add hostname mappings. Use the same values as the docker client `--add-host` parameter.
|
||||||
|
|
@ -377,33 +366,35 @@ It's recommended that you use reverse-DNS notation to prevent your labels from c
|
||||||
### links
|
### links
|
||||||
|
|
||||||
Link to containers in another service. Either specify both the service name and
|
Link to containers in another service. Either specify both the service name and
|
||||||
the link alias (`SERVICE:ALIAS`), or just the service name (which will also be
|
a link alias (`SERVICE:ALIAS`), or just the service name.
|
||||||
used for the alias).
|
|
||||||
|
|
||||||
links:
|
web:
|
||||||
- db
|
links:
|
||||||
- db:database
|
- db
|
||||||
- redis
|
- db:database
|
||||||
|
- redis
|
||||||
|
|
||||||
An entry with the alias' name will be created in `/etc/hosts` inside containers
|
Containers for the linked service will be reachable at a hostname identical to
|
||||||
for this service, e.g:
|
the alias, or the service name if no alias was specified.
|
||||||
|
|
||||||
172.17.2.186 db
|
Links also express dependency between services in the same way as
|
||||||
172.17.2.186 database
|
[depends_on](#depends-on), so they determine the order of service startup.
|
||||||
172.17.2.187 redis
|
|
||||||
|
|
||||||
Environment variables will also be created - see the [environment variable
|
> **Note:** If you define both links and [networks](#networks), services with
|
||||||
reference](env.md) for details.
|
> links between them must share at least one network in common in order to
|
||||||
|
> communicate.
|
||||||
|
|
||||||
### logging
|
### logging
|
||||||
|
|
||||||
Logging configuration for the service. This configuration replaces the previous
|
> [Version 2 file format](#version-2) only. In version 1, use
|
||||||
`log_driver` and `log_opt` keys.
|
> [log_driver](#log_driver) and [log_opt](#log_opt).
|
||||||
|
|
||||||
|
Logging configuration for the service.
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
driver: log_driver
|
driver: syslog
|
||||||
options:
|
options:
|
||||||
syslog-address: "tcp://192.168.0.42:123"
|
syslog-address: "tcp://192.168.0.42:123"
|
||||||
|
|
||||||
The `driver` name specifies a logging driver for the service's
|
The `driver` name specifies a logging driver for the service's
|
||||||
containers, as with the ``--log-driver`` option for docker run
|
containers, as with the ``--log-driver`` option for docker run
|
||||||
|
|
@ -421,21 +412,116 @@ The default value is json-file.
|
||||||
|
|
||||||
Specify logging options for the logging driver with the ``options`` key, as with the ``--log-opt`` option for `docker run`.
|
Specify logging options for the logging driver with the ``options`` key, as with the ``--log-opt`` option for `docker run`.
|
||||||
|
|
||||||
|
Logging options are key-value pairs. An example of `syslog` options:
|
||||||
Logging options are key value pairs. An example of `syslog` options:
|
|
||||||
|
|
||||||
driver: "syslog"
|
driver: "syslog"
|
||||||
options:
|
options:
|
||||||
syslog-address: "tcp://192.168.0.42:123"
|
syslog-address: "tcp://192.168.0.42:123"
|
||||||
|
|
||||||
|
### log_driver
|
||||||
|
|
||||||
|
> [Version 1 file format](#version-1) only. In version 2, use
|
||||||
|
> [logging](#logging).
|
||||||
|
|
||||||
|
Specify a log driver. The default is `json-file`.
|
||||||
|
|
||||||
|
log_driver: syslog
|
||||||
|
|
||||||
|
### log_opt
|
||||||
|
|
||||||
|
> [Version 1 file format](#version-1) only. In version 2, use
|
||||||
|
> [logging](#logging).
|
||||||
|
|
||||||
|
Specify logging options as key-value pairs. An example of `syslog` options:
|
||||||
|
|
||||||
|
log_opt:
|
||||||
|
syslog-address: "tcp://192.168.0.42:123"
|
||||||
|
|
||||||
### net
|
### net
|
||||||
|
|
||||||
Networking mode. Use the same values as the docker client `--net` parameter.
|
> [Version 1 file format](#version-1) only. In version 2, use
|
||||||
|
> [network_mode](#network_mode).
|
||||||
|
|
||||||
|
Network mode. Use the same values as the docker client `--net` parameter.
|
||||||
|
The `container:...` form can take a service name instead of a container name or
|
||||||
|
id.
|
||||||
|
|
||||||
net: "bridge"
|
net: "bridge"
|
||||||
net: "none"
|
|
||||||
net: "container:[name or id]"
|
|
||||||
net: "host"
|
net: "host"
|
||||||
|
net: "none"
|
||||||
|
net: "container:[service name or container name/id]"
|
||||||
|
|
||||||
|
### network_mode
|
||||||
|
|
||||||
|
> [Version 2 file format](#version-2) only. In version 1, use [net](#net).
|
||||||
|
|
||||||
|
Network mode. Use the same values as the docker client `--net` parameter, plus
|
||||||
|
the special form `service:[service name]`.
|
||||||
|
|
||||||
|
network_mode: "bridge"
|
||||||
|
network_mode: "host"
|
||||||
|
network_mode: "none"
|
||||||
|
network_mode: "service:[service name]"
|
||||||
|
network_mode: "container:[container name/id]"
|
||||||
|
|
||||||
|
### networks
|
||||||
|
|
||||||
|
> [Version 2 file format](#version-2) only. In version 1, use [net](#net).
|
||||||
|
|
||||||
|
Networks to join, referencing entries under the
|
||||||
|
[top-level `networks` key](#network-configuration-reference).
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- some-network
|
||||||
|
- other-network
|
||||||
|
|
||||||
|
#### aliases
|
||||||
|
|
||||||
|
Aliases (alternative hostnames) for this service on the network. Other containers on the same network can use either the service name or this alias to connect to one of the service's containers.
|
||||||
|
|
||||||
|
Since `aliases` is network-scoped, the same service can have different aliases on different networks.
|
||||||
|
|
||||||
|
> **Note**: A network-wide alias can be shared by multiple containers, and even by multiple services. If it is, then exactly which container the name will resolve to is not guaranteed.
|
||||||
|
|
||||||
|
The general format is shown here.
|
||||||
|
|
||||||
|
networks:
|
||||||
|
some-network:
|
||||||
|
aliases:
|
||||||
|
- alias1
|
||||||
|
- alias3
|
||||||
|
other-network:
|
||||||
|
aliases:
|
||||||
|
- alias2
|
||||||
|
|
||||||
|
In the example below, three services are provided (`web`, `worker`, and `db`), along with two networks (`new` and `legacy`). The `db` service is reachable at the hostname `db` or `database` on the `new` network, and at `db` or `mysql` on the `legacy` network.
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: ./web
|
||||||
|
networks:
|
||||||
|
- new
|
||||||
|
|
||||||
|
worker:
|
||||||
|
build: ./worker
|
||||||
|
networks:
|
||||||
|
- legacy
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: mysql
|
||||||
|
networks:
|
||||||
|
new:
|
||||||
|
aliases:
|
||||||
|
- database
|
||||||
|
legacy:
|
||||||
|
aliases:
|
||||||
|
- mysql
|
||||||
|
|
||||||
|
networks:
|
||||||
|
new:
|
||||||
|
legacy:
|
||||||
|
|
||||||
### pid
|
### pid
|
||||||
|
|
||||||
|
|
@ -469,9 +555,17 @@ port (a random host port will be chosen).
|
||||||
|
|
||||||
Override the default labeling scheme for each container.
|
Override the default labeling scheme for each container.
|
||||||
|
|
||||||
security_opt:
|
security_opt:
|
||||||
- label:user:USER
|
- label:user:USER
|
||||||
- label:role:ROLE
|
- label:role:ROLE
|
||||||
|
|
||||||
|
### stop_signal
|
||||||
|
|
||||||
|
Sets an alternative signal to stop the container. By default `stop` uses
|
||||||
|
SIGTERM. Setting an alternative signal using `stop_signal` will cause
|
||||||
|
`stop` to send that signal instead.
|
||||||
|
|
||||||
|
stop_signal: SIGUSR1
|
||||||
|
|
||||||
### ulimits
|
### ulimits
|
||||||
|
|
||||||
|
|
@ -479,31 +573,50 @@ Override the default ulimits for a container. You can either specify a single
|
||||||
limit as an integer or soft/hard limits as a mapping.
|
limit as an integer or soft/hard limits as a mapping.
|
||||||
|
|
||||||
|
|
||||||
ulimits:
|
ulimits:
|
||||||
nproc: 65535
|
nproc: 65535
|
||||||
nofile:
|
nofile:
|
||||||
soft: 20000
|
soft: 20000
|
||||||
hard: 40000
|
hard: 40000
|
||||||
|
|
||||||
### volumes, volume\_driver
|
### volumes, volume\_driver
|
||||||
|
|
||||||
Mount paths as volumes, optionally specifying a path on the host machine
|
Mount paths or named volumes, optionally specifying a path on the host machine
|
||||||
(`HOST:CONTAINER`), or an access mode (`HOST:CONTAINER:ro`).
|
(`HOST:CONTAINER`), or an access mode (`HOST:CONTAINER:ro`).
|
||||||
|
For [version 2 files](#version-2), named volumes need to be specified with the
|
||||||
volumes:
|
[top-level `volumes` key](#volume-configuration-reference).
|
||||||
- /var/lib/mysql
|
When using [version 1](#version-1), the Docker Engine will create the named
|
||||||
- ./cache:/tmp/cache
|
volume automatically if it doesn't exist.
|
||||||
- ~/configs:/etc/configs/:ro
|
|
||||||
|
|
||||||
You can mount a relative path on the host, which will expand relative to
|
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
|
the directory of the Compose configuration file being used. Relative paths
|
||||||
should always begin with `.` or `..`.
|
should always begin with `.` or `..`.
|
||||||
|
|
||||||
If you use a volume name (instead of a volume path), you may also specify
|
volumes:
|
||||||
a `volume_driver`.
|
# Just specify a path and let the Engine create a volume
|
||||||
|
- /var/lib/mysql
|
||||||
|
|
||||||
|
# Specify an absolute path mapping
|
||||||
|
- /opt/data:/var/lib/mysql
|
||||||
|
|
||||||
|
# Path on the host, relative to the Compose file
|
||||||
|
- ./cache:/tmp/cache
|
||||||
|
|
||||||
|
# User-relative path
|
||||||
|
- ~/configs:/etc/configs/:ro
|
||||||
|
|
||||||
|
# Named volume
|
||||||
|
- datavolume:/var/lib/mysql
|
||||||
|
|
||||||
|
If you do not use a host path, you may specify a `volume_driver`.
|
||||||
|
|
||||||
volume_driver: mydriver
|
volume_driver: mydriver
|
||||||
|
|
||||||
|
Note that for [version 2 files](#version-2), this driver
|
||||||
|
will not apply to named volumes (you should use the `driver` option when
|
||||||
|
[declaring the volume](#volume-configuration-reference) instead).
|
||||||
|
For [version 1](#version-1), both named volumes and container volumes will
|
||||||
|
use the specified driver.
|
||||||
|
|
||||||
> Note: No path expansion will be done if you have also specified a
|
> Note: No path expansion will be done if you have also specified a
|
||||||
> `volume_driver`.
|
> `volume_driver`.
|
||||||
|
|
@ -519,8 +632,18 @@ specifying read-only access(``ro``) or read-write(``rw``).
|
||||||
|
|
||||||
volumes_from:
|
volumes_from:
|
||||||
- service_name
|
- service_name
|
||||||
- container_name
|
- service_name:ro
|
||||||
- service_name:rw
|
- container:container_name
|
||||||
|
- container:container_name:rw
|
||||||
|
|
||||||
|
> **Note:** The `container:...` formats are only supported in the
|
||||||
|
> [version 2 file format](#version-2). In [version 1](#version-1), you can use
|
||||||
|
> container names without marking them as such:
|
||||||
|
>
|
||||||
|
> - service_name
|
||||||
|
> - service_name:ro
|
||||||
|
> - container_name
|
||||||
|
> - container_name:rw
|
||||||
|
|
||||||
### cpu\_shares, cpu\_quota, cpuset, domainname, hostname, ipc, mac\_address, mem\_limit, memswap\_limit, privileged, read\_only, restart, stdin\_open, tty, user, working\_dir
|
### cpu\_shares, cpu\_quota, cpuset, domainname, hostname, ipc, mac\_address, mem\_limit, memswap\_limit, privileged, read\_only, restart, stdin\_open, tty, user, working\_dir
|
||||||
|
|
||||||
|
|
@ -556,26 +679,347 @@ While it is possible to declare volumes on the fly as part of the service
|
||||||
declaration, this section allows you to create named volumes that can be
|
declaration, this section allows you to create named volumes that can be
|
||||||
reused across multiple services (without relying on `volumes_from`), and are
|
reused across multiple services (without relying on `volumes_from`), and are
|
||||||
easily retrieved and inspected using the docker command line or API.
|
easily retrieved and inspected using the docker command line or API.
|
||||||
See the [docker volume](http://docs.docker.com/reference/commandline/volume/)
|
See the [docker volume](/engine/reference/commandline/volume_create.md)
|
||||||
subcommand documentation for more information.
|
subcommand documentation for more information.
|
||||||
|
|
||||||
### driver
|
### driver
|
||||||
|
|
||||||
Specify which volume driver should be used for this volume. Defaults to
|
Specify which volume driver should be used for this volume. Defaults to
|
||||||
`local`. An exception will be raised if the driver is not available.
|
`local`. The Docker Engine will return an error if the driver is not available.
|
||||||
|
|
||||||
driver: foobar
|
driver: foobar
|
||||||
|
|
||||||
### driver_opts
|
### driver_opts
|
||||||
|
|
||||||
Specify a list of options as key-value pairs to pass to the driver for this
|
Specify a list of options as key-value pairs to pass to the driver for this
|
||||||
volume. Those options are driver dependent - consult the driver's
|
volume. Those options are driver-dependent - consult the driver's
|
||||||
|
documentation for more information. Optional.
|
||||||
|
|
||||||
|
driver_opts:
|
||||||
|
foo: "bar"
|
||||||
|
baz: 1
|
||||||
|
|
||||||
|
## external
|
||||||
|
|
||||||
|
If set to `true`, specifies that this volume has been created outside of
|
||||||
|
Compose. `docker-compose up` will not attempt to create it, and will raise
|
||||||
|
an error if it doesn't exist.
|
||||||
|
|
||||||
|
`external` cannot be used in conjunction with other volume configuration keys
|
||||||
|
(`driver`, `driver_opts`).
|
||||||
|
|
||||||
|
In the example below, instead of attemping to create a volume called
|
||||||
|
`[projectname]_data`, Compose will look for an existing volume simply
|
||||||
|
called `data` and mount it into the `db` service's containers.
|
||||||
|
|
||||||
|
version: '2'
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres
|
||||||
|
volumes:
|
||||||
|
- data:/var/lib/postgres/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
data:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
You can also specify the name of the volume separately from the name used to
|
||||||
|
refer to it within the Compose file:
|
||||||
|
|
||||||
|
volumes
|
||||||
|
data:
|
||||||
|
external:
|
||||||
|
name: actual-name-of-volume
|
||||||
|
|
||||||
|
|
||||||
|
## Network configuration reference
|
||||||
|
|
||||||
|
The top-level `networks` key lets you specify networks to be created. For a full
|
||||||
|
explanation of Compose's use of Docker networking features, see the
|
||||||
|
[Networking guide](networking.md).
|
||||||
|
|
||||||
|
### driver
|
||||||
|
|
||||||
|
Specify which driver should be used for this network.
|
||||||
|
|
||||||
|
The default driver depends on how the Docker Engine you're using is configured,
|
||||||
|
but in most instances it will be `bridge` on a single host and `overlay` on a
|
||||||
|
Swarm.
|
||||||
|
|
||||||
|
The Docker Engine will return an error if the driver is not available.
|
||||||
|
|
||||||
|
driver: overlay
|
||||||
|
|
||||||
|
### driver_opts
|
||||||
|
|
||||||
|
Specify a list of options as key-value pairs to pass to the driver for this
|
||||||
|
network. Those options are driver-dependent - consult the driver's
|
||||||
documentation for more information. Optional.
|
documentation for more information. Optional.
|
||||||
|
|
||||||
driver_opts:
|
driver_opts:
|
||||||
foo: "bar"
|
foo: "bar"
|
||||||
baz: 1
|
baz: 1
|
||||||
|
|
||||||
|
### ipam
|
||||||
|
|
||||||
|
Specify custom IPAM config. This is an object with several properties, each of
|
||||||
|
which is optional:
|
||||||
|
|
||||||
|
- `driver`: Custom IPAM driver, instead of the default.
|
||||||
|
- `config`: A list with zero or more config blocks, each containing any of
|
||||||
|
the following keys:
|
||||||
|
- `subnet`: Subnet in CIDR format that represents a network segment
|
||||||
|
- `ip_range`: Range of IPs from which to allocate container IPs
|
||||||
|
- `gateway`: IPv4 or IPv6 gateway for the master subnet
|
||||||
|
- `aux_addresses`: Auxiliary IPv4 or IPv6 addresses used by Network driver,
|
||||||
|
as a mapping from hostname to IP
|
||||||
|
|
||||||
|
A full example:
|
||||||
|
|
||||||
|
ipam:
|
||||||
|
driver: default
|
||||||
|
config:
|
||||||
|
- subnet: 172.28.0.0/16
|
||||||
|
ip_range: 172.28.5.0/24
|
||||||
|
gateway: 172.28.5.254
|
||||||
|
aux_addresses:
|
||||||
|
host1: 172.28.1.5
|
||||||
|
host2: 172.28.1.6
|
||||||
|
host3: 172.28.1.7
|
||||||
|
|
||||||
|
### external
|
||||||
|
|
||||||
|
If set to `true`, specifies that this network has been created outside of
|
||||||
|
Compose. `docker-compose up` will not attempt to create it, and will raise
|
||||||
|
an error if it doesn't exist.
|
||||||
|
|
||||||
|
`external` cannot be used in conjunction with other network configuration keys
|
||||||
|
(`driver`, `driver_opts`, `ipam`).
|
||||||
|
|
||||||
|
In the example below, `proxy` is the gateway to the outside world. Instead of
|
||||||
|
attemping to create a network called `[projectname]_outside`, Compose will
|
||||||
|
look for an existing network simply called `outside` and connect the `proxy`
|
||||||
|
service's containers to it.
|
||||||
|
|
||||||
|
version: '2'
|
||||||
|
|
||||||
|
services:
|
||||||
|
proxy:
|
||||||
|
build: ./proxy
|
||||||
|
networks:
|
||||||
|
- outside
|
||||||
|
- default
|
||||||
|
app:
|
||||||
|
build: ./app
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
|
||||||
|
networks:
|
||||||
|
outside:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
You can also specify the name of the network separately from the name used to
|
||||||
|
refer to it within the Compose file:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
outside:
|
||||||
|
external:
|
||||||
|
name: actual-name-of-network
|
||||||
|
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
There are two versions of the Compose file format:
|
||||||
|
|
||||||
|
- Version 1, the legacy format. This is specified by omitting a `version` key at
|
||||||
|
the root of the YAML.
|
||||||
|
- Version 2, the recommended format. This is specified with a `version: '2'` entry
|
||||||
|
at the root of the YAML.
|
||||||
|
|
||||||
|
To move your project from version 1 to 2, see the [Upgrading](#upgrading)
|
||||||
|
section.
|
||||||
|
|
||||||
|
> **Note:** If you're using
|
||||||
|
> [multiple Compose files](extends.md#different-environments) or
|
||||||
|
> [extending services](extends.md#extending-services), each file must be of the
|
||||||
|
> same version - you cannot mix version 1 and 2 in a single project.
|
||||||
|
|
||||||
|
Several things differ depending on which version you use:
|
||||||
|
|
||||||
|
- The structure and permitted configuration keys
|
||||||
|
- The minimum Docker Engine version you must be running
|
||||||
|
- Compose's behaviour with regards to networking
|
||||||
|
|
||||||
|
These differences are explained below.
|
||||||
|
|
||||||
|
|
||||||
|
### Version 1
|
||||||
|
|
||||||
|
Compose files that do not declare a version are considered "version 1". In
|
||||||
|
those files, all the [services](#service-configuration-reference) are declared
|
||||||
|
at the root of the document.
|
||||||
|
|
||||||
|
Version 1 is supported by **Compose up to 1.6.x**. It will be deprecated in a
|
||||||
|
future Compose release.
|
||||||
|
|
||||||
|
Version 1 files cannot declare named
|
||||||
|
[volumes](#volume-configuration-reference), [networks](networking.md) or
|
||||||
|
[build arguments](#args).
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
volumes:
|
||||||
|
- .:/code
|
||||||
|
links:
|
||||||
|
- redis
|
||||||
|
redis:
|
||||||
|
image: redis
|
||||||
|
|
||||||
|
|
||||||
|
### Version 2
|
||||||
|
|
||||||
|
Compose files using the version 2 syntax must indicate the version number at
|
||||||
|
the root of the document. All [services](#service-configuration-reference)
|
||||||
|
must be declared under the `services` key.
|
||||||
|
|
||||||
|
Version 2 files are supported by **Compose 1.6.0+** and require a Docker Engine
|
||||||
|
of version **1.10.0+**.
|
||||||
|
|
||||||
|
Named [volumes](#volume-configuration-reference) can be declared under the
|
||||||
|
`volumes` key, and [networks](#network-configuration-reference) can be declared
|
||||||
|
under the `networks` key.
|
||||||
|
|
||||||
|
Simple example:
|
||||||
|
|
||||||
|
version: '2'
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
volumes:
|
||||||
|
- .:/code
|
||||||
|
redis:
|
||||||
|
image: redis
|
||||||
|
|
||||||
|
A more extended example, defining volumes and networks:
|
||||||
|
|
||||||
|
version: '2'
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
volumes:
|
||||||
|
- .:/code
|
||||||
|
networks:
|
||||||
|
- front-tier
|
||||||
|
- back-tier
|
||||||
|
redis:
|
||||||
|
image: redis
|
||||||
|
volumes:
|
||||||
|
- redis-data:/var/lib/redis
|
||||||
|
networks:
|
||||||
|
- back-tier
|
||||||
|
volumes:
|
||||||
|
redis-data:
|
||||||
|
driver: local
|
||||||
|
networks:
|
||||||
|
front-tier:
|
||||||
|
driver: bridge
|
||||||
|
back-tier:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
|
||||||
|
### Upgrading
|
||||||
|
|
||||||
|
In the majority of cases, moving from version 1 to 2 is a very simple process:
|
||||||
|
|
||||||
|
1. Indent the whole file by one level and put a `services:` key at the top.
|
||||||
|
2. Add a `version: '2'` line at the top of the file.
|
||||||
|
|
||||||
|
It's more complicated if you're using particular configuration features:
|
||||||
|
|
||||||
|
- `dockerfile`: This now lives under the `build` key:
|
||||||
|
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile-alternate
|
||||||
|
|
||||||
|
- `log_driver`, `log_opt`: These now live under the `logging` key:
|
||||||
|
|
||||||
|
logging:
|
||||||
|
driver: syslog
|
||||||
|
options:
|
||||||
|
syslog-address: "tcp://192.168.0.42:123"
|
||||||
|
|
||||||
|
- `links` with environment variables: As documented in the
|
||||||
|
[environment variables reference](link-env-deprecated.md), environment variables
|
||||||
|
created by
|
||||||
|
links have been deprecated for some time. In the new Docker network system,
|
||||||
|
they have been removed. You should either connect directly to the
|
||||||
|
appropriate hostname or set the relevant environment variable yourself,
|
||||||
|
using the link hostname:
|
||||||
|
|
||||||
|
web:
|
||||||
|
links:
|
||||||
|
- db
|
||||||
|
environment:
|
||||||
|
- DB_PORT=tcp://db:5432
|
||||||
|
|
||||||
|
- `external_links`: Compose uses Docker networks when running version 2
|
||||||
|
projects, so links behave slightly differently. In particular, two
|
||||||
|
containers must be connected to at least one network in common in order to
|
||||||
|
communicate, even if explicitly linked together.
|
||||||
|
|
||||||
|
Either connect the external container to your app's
|
||||||
|
[default network](networking.md), or connect both the external container and
|
||||||
|
your service's containers to an
|
||||||
|
[external network](networking.md#using-a-pre-existing-network).
|
||||||
|
|
||||||
|
- `net`: This is now replaced by [network_mode](#network_mode):
|
||||||
|
|
||||||
|
net: host -> network_mode: host
|
||||||
|
net: bridge -> network_mode: bridge
|
||||||
|
net: none -> network_mode: none
|
||||||
|
|
||||||
|
If you're using `net: "container:[service name]"`, you must now use
|
||||||
|
`network_mode: "service:[service name]"` instead.
|
||||||
|
|
||||||
|
net: "container:web" -> network_mode: "service:web"
|
||||||
|
|
||||||
|
If you're using `net: "container:[container name/id]"`, the value does not
|
||||||
|
need to change.
|
||||||
|
|
||||||
|
net: "container:cont-name" -> network_mode: "container:cont-name"
|
||||||
|
net: "container:abc12345" -> network_mode: "container:abc12345"
|
||||||
|
|
||||||
|
- `volumes` with named volumes: these must now be explicitly declared in a
|
||||||
|
top-level `volumes` section of your Compose file. If a service mounts a
|
||||||
|
named volume called `data`, you must declare a `data` volume in your
|
||||||
|
top-level `volumes` section. The whole file might look like this:
|
||||||
|
|
||||||
|
version: '2'
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres
|
||||||
|
volumes:
|
||||||
|
- data:/var/lib/postgresql/data
|
||||||
|
volumes:
|
||||||
|
data: {}
|
||||||
|
|
||||||
|
By default, Compose creates a volume whose name is prefixed with your
|
||||||
|
project name. If you want it to just be called `data`, declared it as
|
||||||
|
external:
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
data:
|
||||||
|
external: true
|
||||||
|
|
||||||
## Variable substitution
|
## Variable substitution
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
<!--[metadata]>
|
<!--[metadata]>
|
||||||
+++
|
+++
|
||||||
title = "Quickstart Guide: Compose and Django"
|
title = "Quickstart: Compose and Django"
|
||||||
description = "Getting started with Docker Compose and Django"
|
description = "Getting started with Docker Compose and Django"
|
||||||
keywords = ["documentation, docs, docker, compose, orchestration, containers"]
|
keywords = ["documentation, docs, docker, compose, orchestration, containers"]
|
||||||
[menu.main]
|
[menu.main]
|
||||||
parent="smn_workw_compose"
|
parent="workw_compose"
|
||||||
weight=4
|
weight=4
|
||||||
+++
|
+++
|
||||||
<![end-metadata]-->
|
<![end-metadata]-->
|
||||||
|
|
||||||
|
|
||||||
# Quickstart Guide: Compose and Django
|
# Quickstart: Compose and Django
|
||||||
|
|
||||||
This quick-start guide demonstrates how to use Compose to set up and run a
|
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
|
simple Django/PostgreSQL app. Before starting, you'll need to have
|
||||||
|
|
@ -72,17 +72,19 @@ and a `docker-compose.yml` file.
|
||||||
|
|
||||||
9. Add the following configuration to the file.
|
9. Add the following configuration to the file.
|
||||||
|
|
||||||
db:
|
version: '2'
|
||||||
image: postgres
|
services:
|
||||||
web:
|
db:
|
||||||
build: .
|
image: postgres
|
||||||
command: python manage.py runserver 0.0.0.0:8000
|
web:
|
||||||
volumes:
|
build: .
|
||||||
- .:/code
|
command: python manage.py runserver 0.0.0.0:8000
|
||||||
ports:
|
volumes:
|
||||||
- "8000:8000"
|
- .:/code
|
||||||
links:
|
ports:
|
||||||
- db
|
- "8000:8000"
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
This file defines two services: The `db` service and the `web` service.
|
This file defines two services: The `db` service and the `web` service.
|
||||||
|
|
||||||
|
|
@ -129,7 +131,7 @@ In this step, you create a Django started project by building the image from the
|
||||||
|
|
||||||
In this section, you set up the database connection for Django.
|
In this section, you set up the database connection for Django.
|
||||||
|
|
||||||
1. In your project dirctory, edit the `composeexample/settings.py` file.
|
1. In your project directory, edit the `composeexample/settings.py` file.
|
||||||
|
|
||||||
2. Replace the `DATABASES = ...` with the following:
|
2. Replace the `DATABASES = ...` with the following:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
<!--[metadata]>
|
<!--[metadata]>
|
||||||
+++
|
+++
|
||||||
title = "Extending services in Compose"
|
title = "Extending Services in Compose"
|
||||||
description = "How to use Docker Compose's extends keyword to share configuration between files and projects"
|
description = "How to use Docker Compose's extends keyword to share configuration between files and projects"
|
||||||
keywords = ["fig, composition, compose, docker, orchestration, documentation, docs"]
|
keywords = ["fig, composition, compose, docker, orchestration, documentation, docs"]
|
||||||
[menu.main]
|
[menu.main]
|
||||||
parent="smn_workw_compose"
|
parent="workw_compose"
|
||||||
weight=2
|
weight=20
|
||||||
+++
|
+++
|
||||||
<![end-metadata]-->
|
<![end-metadata]-->
|
||||||
|
|
||||||
|
|
@ -32,17 +32,14 @@ contains your base configuration. The override file, as its name implies, can
|
||||||
contain configuration overrides for existing services or entirely new
|
contain configuration overrides for existing services or entirely new
|
||||||
services.
|
services.
|
||||||
|
|
||||||
If a service is defined in both files, Compose merges the configurations using
|
If a service is defined in both files Compose merges the configurations using
|
||||||
the same rules as the `extends` field (see [Adding and overriding
|
the rules described in [Adding and overriding
|
||||||
configuration](#adding-and-overriding-configuration)), with one exception. If a
|
configuration](#adding-and-overriding-configuration).
|
||||||
service contains `links` or `volumes_from` those fields are copied over and
|
|
||||||
replace any values in the original service, in the same way single-valued fields
|
|
||||||
are copied.
|
|
||||||
|
|
||||||
To use multiple override files, or an override file with a different name, you
|
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
|
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`
|
the order they're specified on the command line. See the [`docker-compose`
|
||||||
command reference](./reference/docker-compose.md) for more information about
|
command reference](./reference/overview.md) for more information about
|
||||||
using `-f`.
|
using `-f`.
|
||||||
|
|
||||||
When you use multiple configuration files, you must make sure all paths in the
|
When you use multiple configuration files, you must make sure all paths in the
|
||||||
|
|
@ -176,10 +173,12 @@ 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
|
options. Using `extends` you can define a common set of service options in one
|
||||||
place and refer to it from anywhere.
|
place and refer to it from anywhere.
|
||||||
|
|
||||||
> **Note:** `links` and `volumes_from` are never shared between services using
|
> **Note:** `links`, `volumes_from`, and `depends_on` are never shared between
|
||||||
> `extends`. See
|
> services using >`extends`. These exceptions exist to avoid
|
||||||
> [Adding and overriding configuration](#adding-and-overriding-configuration)
|
> implicit dependencies—you always define `links` and `volumes_from`
|
||||||
> for more information.
|
> 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
|
### Understand the extends configuration
|
||||||
|
|
||||||
|
|
@ -275,13 +274,7 @@ common configuration:
|
||||||
|
|
||||||
## Adding and overriding configuration
|
## Adding and overriding configuration
|
||||||
|
|
||||||
Compose copies configurations from the original service over to the local one,
|
Compose copies configurations from the original service over to the local one.
|
||||||
**except** for `links` and `volumes_from`. These exceptions exist to avoid
|
|
||||||
implicit dependencies—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.
|
|
||||||
|
|
||||||
If a configuration option is defined in both the original service the local
|
If a configuration option is defined in both the original service the local
|
||||||
service, the local value *replaces* or *extends* the original value.
|
service, the local value *replaces* or *extends* the original value.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@ title = "Frequently Asked Questions"
|
||||||
description = "Docker Compose FAQ"
|
description = "Docker Compose FAQ"
|
||||||
keywords = "documentation, docs, docker, compose, faq"
|
keywords = "documentation, docs, docker, compose, faq"
|
||||||
[menu.main]
|
[menu.main]
|
||||||
parent="smn_workw_compose"
|
identifier="faq.compose"
|
||||||
weight=9
|
parent="workw_compose"
|
||||||
|
weight=90
|
||||||
+++
|
+++
|
||||||
<![end-metadata]-->
|
<![end-metadata]-->
|
||||||
|
|
||||||
|
|
@ -50,8 +51,8 @@ handling `SIGTERM` properly.
|
||||||
Compose uses the project name to create unique identifiers for all of a
|
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,
|
project's containers and other resources. To run multiple copies of a project,
|
||||||
set a custom project name using the [`-p` command line
|
set a custom project name using the [`-p` command line
|
||||||
option](./reference/docker-compose.md) or the [`COMPOSE_PROJECT_NAME`
|
option](./reference/overview.md) or the [`COMPOSE_PROJECT_NAME`
|
||||||
environment variable](./reference/overview.md#compose-project-name).
|
environment variable](./reference/envvars.md#compose-project-name).
|
||||||
|
|
||||||
## What's the difference between `up`, `run`, and `start`?
|
## What's the difference between `up`, `run`, and `start`?
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ title = "Getting Started"
|
||||||
description = "Getting started with Docker Compose"
|
description = "Getting started with Docker Compose"
|
||||||
keywords = ["documentation, docs, docker, compose, orchestration, containers"]
|
keywords = ["documentation, docs, docker, compose, orchestration, containers"]
|
||||||
[menu.main]
|
[menu.main]
|
||||||
parent="smn_workw_compose"
|
parent="workw_compose"
|
||||||
weight=3
|
weight=-85
|
||||||
+++
|
+++
|
||||||
<![end-metadata]-->
|
<![end-metadata]-->
|
||||||
|
|
||||||
|
|
@ -95,16 +95,19 @@ Define a set of services using `docker-compose.yml`:
|
||||||
1. Create a file called docker-compose.yml in your project directory and add
|
1. Create a file called docker-compose.yml in your project directory and add
|
||||||
the following:
|
the following:
|
||||||
|
|
||||||
web:
|
|
||||||
build: .
|
version: '2'
|
||||||
ports:
|
services:
|
||||||
- "5000:5000"
|
web:
|
||||||
volumes:
|
build: .
|
||||||
- .:/code
|
ports:
|
||||||
links:
|
- "5000:5000"
|
||||||
- redis
|
volumes:
|
||||||
redis:
|
- .:/code
|
||||||
image: redis
|
depends_on:
|
||||||
|
- redis
|
||||||
|
redis:
|
||||||
|
image: redis
|
||||||
|
|
||||||
This Compose file defines two services, `web` and `redis`. The web service:
|
This Compose file defines two services, `web` and `redis`. The web service:
|
||||||
|
|
||||||
|
|
|
||||||
176
docs/index.md
176
docs/index.md
|
|
@ -1,66 +1,21 @@
|
||||||
<!--[metadata]>
|
<!--[metadata]>
|
||||||
+++
|
+++
|
||||||
title = "Overview of Docker Compose"
|
title = "Docker Compose"
|
||||||
description = "Introduction and Overview of Compose"
|
description = "Introduction and Overview of Compose"
|
||||||
keywords = ["documentation, docs, docker, compose, orchestration, containers"]
|
keywords = ["documentation, docs, docker, compose, orchestration, containers"]
|
||||||
[menu.main]
|
[menu.main]
|
||||||
parent="smn_workw_compose"
|
identifier="workw_compose"
|
||||||
|
weight=-70
|
||||||
+++
|
+++
|
||||||
<![end-metadata]-->
|
<![end-metadata]-->
|
||||||
|
|
||||||
|
|
||||||
# Overview of Docker Compose
|
# Docker Compose
|
||||||
|
|
||||||
Compose is a tool for defining and running multi-container Docker applications.
|
Compose is a tool for defining and running multi-container Docker applications. To learn more about Compose refer to the following documentation:
|
||||||
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
|
- [Compose Overview](overview.md)
|
||||||
CI workflows. You can learn more about each case in
|
- [Install Compose](install.md)
|
||||||
[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:
|
|
||||||
driver: default
|
|
||||||
|
|
||||||
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)
|
- [Getting Started](gettingstarted.md)
|
||||||
- [Get started with Django](django.md)
|
- [Get started with Django](django.md)
|
||||||
- [Get started with Rails](rails.md)
|
- [Get started with Rails](rails.md)
|
||||||
|
|
@ -69,123 +24,6 @@ Compose has commands for managing the whole lifecycle of your application:
|
||||||
- [Command line reference](./reference/index.md)
|
- [Command line reference](./reference/index.md)
|
||||||
- [Compose file reference](compose-file.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/docker-compose.md) or the
|
|
||||||
[`COMPOSE_PROJECT_NAME` environment variable](./reference/overview.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
|
To see a detailed list of changes for past and current releases of Docker
|
||||||
Compose, please refer to the
|
Compose, please refer to the
|
||||||
[CHANGELOG](https://github.com/docker/compose/blob/master/CHANGELOG.md).
|
[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/).
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
<!--[metadata]>
|
<!--[metadata]>
|
||||||
+++
|
+++
|
||||||
title = "Docker Compose"
|
title = "Install Compose"
|
||||||
description = "How to install Docker Compose"
|
description = "How to install Docker Compose"
|
||||||
keywords = ["compose, orchestration, install, installation, docker, documentation"]
|
keywords = ["compose, orchestration, install, installation, docker, documentation"]
|
||||||
[menu.main]
|
[menu.main]
|
||||||
parent="mn_install"
|
parent="workw_compose"
|
||||||
weight=4
|
weight=-90
|
||||||
+++
|
+++
|
||||||
<![end-metadata]-->
|
<![end-metadata]-->
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@ which the release page specifies, in your terminal.
|
||||||
|
|
||||||
The following is an example command illustrating the format:
|
The following is an example command illustrating the format:
|
||||||
|
|
||||||
curl -L https://github.com/docker/compose/releases/download/1.5.2/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
|
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
|
If you have problems installing with `curl`, see
|
||||||
[Alternative Install Options](#alternative-install-options).
|
[Alternative Install Options](#alternative-install-options).
|
||||||
|
|
@ -54,7 +54,7 @@ which the release page specifies, in your terminal.
|
||||||
7. Test the installation.
|
7. Test the installation.
|
||||||
|
|
||||||
$ docker-compose --version
|
$ docker-compose --version
|
||||||
docker-compose version: 1.5.2
|
docker-compose version: 1.6.1
|
||||||
|
|
||||||
|
|
||||||
## Alternative install options
|
## Alternative install options
|
||||||
|
|
@ -77,7 +77,7 @@ to get started.
|
||||||
Compose can also be run inside a container, from a small bash script wrapper.
|
Compose can also be run inside a container, from a small bash script wrapper.
|
||||||
To install compose as a container run:
|
To install compose as a container run:
|
||||||
|
|
||||||
$ curl -L https://github.com/docker/compose/releases/download/1.5.2/run.sh > /usr/local/bin/docker-compose
|
$ 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
|
$ chmod +x /usr/local/bin/docker-compose
|
||||||
|
|
||||||
## Master builds
|
## Master builds
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,20 @@
|
||||||
<!--[metadata]>
|
<!--[metadata]>
|
||||||
+++
|
+++
|
||||||
title = "Compose environment variables reference"
|
title = "Link Environment Variables"
|
||||||
description = "Compose CLI reference"
|
description = "Compose CLI reference"
|
||||||
keywords = ["fig, composition, compose, docker, orchestration, cli, reference"]
|
keywords = ["fig, composition, compose, docker, orchestration, cli, reference"]
|
||||||
|
aliases = ["/compose/env"]
|
||||||
[menu.main]
|
[menu.main]
|
||||||
parent="smn_compose_ref"
|
parent="workw_compose"
|
||||||
weight=3
|
weight=89
|
||||||
+++
|
+++
|
||||||
<![end-metadata]-->
|
<![end-metadata]-->
|
||||||
|
|
||||||
# Compose environment variables reference
|
# 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.
|
> **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.
|
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.
|
||||||
|
|
||||||
|
|
@ -4,91 +4,149 @@ title = "Networking in Compose"
|
||||||
description = "How Compose sets up networking between containers"
|
description = "How Compose sets up networking between containers"
|
||||||
keywords = ["documentation, docs, docker, compose, orchestration, containers, networking"]
|
keywords = ["documentation, docs, docker, compose, orchestration, containers, networking"]
|
||||||
[menu.main]
|
[menu.main]
|
||||||
parent="smn_workw_compose"
|
parent="workw_compose"
|
||||||
weight=6
|
weight=21
|
||||||
+++
|
+++
|
||||||
<![end-metadata]-->
|
<![end-metadata]-->
|
||||||
|
|
||||||
|
|
||||||
# Networking in Compose
|
# Networking in Compose
|
||||||
|
|
||||||
> **Note:** Compose's networking support is experimental, and must be explicitly enabled with the `docker-compose --x-networking` flag.
|
> **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.
|
||||||
|
|
||||||
Compose sets up a single default
|
By default Compose sets up a single
|
||||||
[network](/engine/reference/commandline/network_create.md) for your app. Each
|
[network](/engine/reference/commandline/network_create.md) for your app. Each
|
||||||
container for a service joins the default network and is both *reachable* by
|
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
|
other containers on that network, and *discoverable* by them at a hostname
|
||||||
identical to the container name.
|
identical to the container name.
|
||||||
|
|
||||||
> **Note:** Your app's network is given the same name as the "project name", which is based on the name of the directory it lives in. See the [Command line overview](reference/docker-compose.md) for how to override it.
|
> **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:
|
For example, suppose your app is in a directory called `myapp`, and your `docker-compose.yml` looks like this:
|
||||||
|
|
||||||
web:
|
version: '2'
|
||||||
build: .
|
|
||||||
ports:
|
|
||||||
- "8000:8000"
|
|
||||||
db:
|
|
||||||
image: postgres
|
|
||||||
|
|
||||||
When you run `docker-compose --x-networking up`, the following happens:
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
db:
|
||||||
|
image: postgres
|
||||||
|
|
||||||
1. A network called `myapp` is created.
|
When you run `docker-compose up`, the following happens:
|
||||||
2. A container is created using `web`'s configuration. It joins the network
|
|
||||||
`myapp` under the name `myapp_web_1`.
|
|
||||||
3. A container is created using `db`'s configuration. It joins the network
|
|
||||||
`myapp` under the name `myapp_db_1`.
|
|
||||||
|
|
||||||
Each container can now look up the hostname `myapp_web_1` or `myapp_db_1` and
|
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
|
get back the appropriate container's IP address. For example, `web`'s
|
||||||
application code could connect to the URL `postgres://myapp_db_1:5432` and start
|
application code could connect to the URL `postgres://db:5432` and start
|
||||||
using the Postgres database.
|
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.
|
Because `web` explicitly maps a port, it's also accessible from the outside world via port 8000 on your Docker host's network interface.
|
||||||
|
|
||||||
> **Note:** in the next release there will be additional aliases for the
|
|
||||||
> container, including a short name without the project name and container
|
|
||||||
> index. The full container name will remain as one of the alias for backwards
|
|
||||||
> compatibility.
|
|
||||||
|
|
||||||
## Updating containers
|
## 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 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.
|
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.
|
||||||
|
|
||||||
## Configure how services are published
|
|
||||||
|
|
||||||
By default, containers for each service are published on the network with the
|
|
||||||
container name. If you want to change the name, or stop containers from being
|
|
||||||
discoverable at all, you can use the `container_name` option:
|
|
||||||
|
|
||||||
web:
|
|
||||||
build: .
|
|
||||||
container_name: "my-web-application"
|
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
Docker links are a one-way, single-host communication system. They should now be considered deprecated, and you should update your app to use networking instead. In the majority of cases, this will simply involve removing the `links` sections from your `docker-compose.yml`.
|
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`:
|
||||||
|
|
||||||
## Specifying the network driver
|
version: '2'
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
links:
|
||||||
|
- "db:database"
|
||||||
|
db:
|
||||||
|
image: postgres
|
||||||
|
|
||||||
By default, Compose uses the `bridge` driver when creating the app’s network. The Docker Engine provides one other driver out-of-the-box: `overlay`, which implements secure communication between containers on different hosts (see the next section for how to set up and use the `overlay` driver). Docker also allows you to install [custom network drivers](/engine/extend/plugins_network.md).
|
See the [links reference](compose-file.md#links) for more information.
|
||||||
|
|
||||||
You can specify which one to use with the `--x-network-driver` flag:
|
|
||||||
|
|
||||||
$ docker-compose --x-networking --x-network-driver=overlay up
|
|
||||||
|
|
||||||
<!--[metadata]>
|
|
||||||
## Multi-host networking
|
## Multi-host networking
|
||||||
|
|
||||||
(TODO: talk about Swarm and the overlay driver)
|
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.
|
||||||
<![end-metadata]-->
|
|
||||||
|
|
||||||
## Custom container network modes
|
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.
|
||||||
|
|
||||||
Compose allows you to specify a custom network mode for a service with the `net` option - for example, `net: "host"` specifies that its containers should use the same network namespace as the Docker host, and `net: "none"` specifies that they should have no networking capabilities.
|
## Specifying custom networks
|
||||||
|
|
||||||
If a service specifies the `net` option, its containers will *not* join the app’s network and will not be able to communicate with other services in the app.
|
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.
|
||||||
|
|
||||||
If *all* services in an app specify the `net` option, a network will not be created at all.
|
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
191
docs/overview.md
Normal 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/).
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
<!--[metadata]>
|
<!--[metadata]>
|
||||||
+++
|
+++
|
||||||
title = "Using Compose in production"
|
title = "Using Compose in Production"
|
||||||
description = "Guide to using Docker Compose in production"
|
description = "Guide to using Docker Compose in production"
|
||||||
keywords = ["documentation, docs, docker, compose, orchestration, containers, production"]
|
keywords = ["documentation, docs, docker, compose, orchestration, containers, production"]
|
||||||
[menu.main]
|
[menu.main]
|
||||||
parent="smn_workw_compose"
|
parent="workw_compose"
|
||||||
weight=1
|
weight=22
|
||||||
+++
|
+++
|
||||||
<![end-metadata]-->
|
<![end-metadata]-->
|
||||||
|
|
||||||
|
|
@ -60,7 +60,7 @@ recreating any services which `web` depends on.
|
||||||
You can use Compose to deploy an app to a remote Docker host by setting the
|
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
|
`DOCKER_HOST`, `DOCKER_TLS_VERIFY`, and `DOCKER_CERT_PATH` environment variables
|
||||||
appropriately. For tasks like this,
|
appropriately. For tasks like this,
|
||||||
[Docker Machine](https://docs.docker.com/machine/) makes managing local and
|
[Docker Machine](/machine/overview) makes managing local and
|
||||||
remote Docker hosts very easy, and is recommended even if you're not deploying
|
remote Docker hosts very easy, and is recommended even if you're not deploying
|
||||||
remotely.
|
remotely.
|
||||||
|
|
||||||
|
|
@ -69,14 +69,12 @@ commands will work with no further configuration.
|
||||||
|
|
||||||
### Running Compose on a Swarm cluster
|
### Running Compose on a Swarm cluster
|
||||||
|
|
||||||
[Docker Swarm](https://docs.docker.com/swarm/), a Docker-native clustering
|
[Docker Swarm](/swarm/overview), a Docker-native clustering
|
||||||
system, exposes the same API as a single Docker host, which means you can use
|
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 against a Swarm instance and run your apps across multiple hosts.
|
||||||
|
|
||||||
Compose/Swarm integration is still in the experimental stage, and Swarm is still
|
Compose/Swarm integration is still in the experimental stage, but if you'd like
|
||||||
in beta, but if you'd like to explore and experiment, check out the <a
|
to explore and experiment, check out the [integration guide](swarm.md).
|
||||||
href="https://github.com/docker/compose/blob/master/SWARM.md">integration
|
|
||||||
guide</a>.
|
|
||||||
|
|
||||||
## Compose documentation
|
## Compose documentation
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
<!--[metadata]>
|
<!--[metadata]>
|
||||||
+++
|
+++
|
||||||
title = "Quickstart Guide: Compose and Rails"
|
title = "Quickstart: Compose and Rails"
|
||||||
description = "Getting started with Docker Compose and Rails"
|
description = "Getting started with Docker Compose and Rails"
|
||||||
keywords = ["documentation, docs, docker, compose, orchestration, containers"]
|
keywords = ["documentation, docs, docker, compose, orchestration, containers"]
|
||||||
[menu.main]
|
[menu.main]
|
||||||
parent="smn_workw_compose"
|
parent="workw_compose"
|
||||||
weight=5
|
weight=5
|
||||||
+++
|
+++
|
||||||
<![end-metadata]-->
|
<![end-metadata]-->
|
||||||
|
|
||||||
## Quickstart Guide: Compose and Rails
|
## 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).
|
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).
|
||||||
|
|
||||||
|
|
@ -43,17 +43,19 @@ You'll need an empty `Gemfile.lock` in order to build our `Dockerfile`.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
db:
|
version: '2'
|
||||||
image: postgres
|
services:
|
||||||
web:
|
db:
|
||||||
build: .
|
image: postgres
|
||||||
command: bundle exec rails s -p 3000 -b '0.0.0.0'
|
web:
|
||||||
volumes:
|
build: .
|
||||||
- .:/myapp
|
command: bundle exec rails s -p 3000 -b '0.0.0.0'
|
||||||
ports:
|
volumes:
|
||||||
- "3000:3000"
|
- .:/myapp
|
||||||
links:
|
ports:
|
||||||
- db
|
- "3000:3000"
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
### Build the project
|
### Build the project
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
<!--[metadata]>
|
|
||||||
+++
|
|
||||||
title = "docker-compose"
|
|
||||||
description = "docker-compose Command Binary"
|
|
||||||
keywords = ["fig, composition, compose, docker, orchestration, cli, docker-compose"]
|
|
||||||
[menu.main]
|
|
||||||
parent = "smn_compose_cli"
|
|
||||||
weight=-2
|
|
||||||
+++
|
|
||||||
<![end-metadata]-->
|
|
||||||
|
|
||||||
|
|
||||||
# docker-compose Command
|
|
||||||
|
|
||||||
```
|
|
||||||
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
|
|
||||||
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 subdirectories 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,
|
|
||||||
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](overview.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](
|
|
||||||
overview.md#compose-project-name)
|
|
||||||
|
|
||||||
|
|
||||||
## Where to go next
|
|
||||||
|
|
||||||
* [CLI environment variables](overview.md)
|
|
||||||
* [Command line reference](index.md)
|
|
||||||
|
|
@ -4,7 +4,7 @@ title = "down"
|
||||||
description = "down"
|
description = "down"
|
||||||
keywords = ["fig, composition, compose, docker, orchestration, cli, down"]
|
keywords = ["fig, composition, compose, docker, orchestration, cli, down"]
|
||||||
[menu.main]
|
[menu.main]
|
||||||
identifier="build.compose"
|
identifier="down.compose"
|
||||||
parent = "smn_compose_cli"
|
parent = "smn_compose_cli"
|
||||||
+++
|
+++
|
||||||
<![end-metadata]-->
|
<![end-metadata]-->
|
||||||
|
|
|
||||||
78
docs/reference/envvars.md
Normal file
78
docs/reference/envvars.md
Normal 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)
|
||||||
|
|
@ -1,18 +1,20 @@
|
||||||
<!--[metadata]>
|
<!--[metadata]>
|
||||||
+++
|
+++
|
||||||
title = "Compose CLI reference"
|
title = "Command-line Reference"
|
||||||
description = "Compose CLI reference"
|
description = "Compose CLI reference"
|
||||||
keywords = ["fig, composition, compose, docker, orchestration, cli, reference"]
|
keywords = ["fig, composition, compose, docker, orchestration, cli, reference"]
|
||||||
[menu.main]
|
[menu.main]
|
||||||
identifier = "smn_compose_cli"
|
identifier = "smn_compose_cli"
|
||||||
parent = "smn_compose_ref"
|
parent = "workw_compose"
|
||||||
|
weight=80
|
||||||
+++
|
+++
|
||||||
<![end-metadata]-->
|
<![end-metadata]-->
|
||||||
|
|
||||||
## Compose CLI reference
|
## Compose command-line reference
|
||||||
|
|
||||||
The following pages describe the usage information for the [docker-compose](docker-compose.md) subcommands. You can also see this information by running `docker-compose [SUBCOMMAND] --help` from the command line.
|
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)
|
* [build](build.md)
|
||||||
* [config](config.md)
|
* [config](config.md)
|
||||||
* [create](create.md)
|
* [create](create.md)
|
||||||
|
|
@ -36,5 +38,5 @@ The following pages describe the usage information for the [docker-compose](dock
|
||||||
|
|
||||||
## Where to go next
|
## Where to go next
|
||||||
|
|
||||||
* [CLI environment variables](overview.md)
|
* [CLI environment variables](envvars.md)
|
||||||
* [docker-compose Command](docker-compose.md)
|
* [docker-compose Command](overview.md)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
<!--[metadata]>
|
<!--[metadata]>
|
||||||
+++
|
+++
|
||||||
title = "Introduction to the CLI"
|
title = "Overview of docker-compose CLI"
|
||||||
description = "Introduction to the CLI"
|
description = "Overview of docker-compose CLI"
|
||||||
keywords = ["fig, composition, compose, docker, orchestration, cli, reference"]
|
keywords = ["fig, composition, compose, docker, orchestration, cli, docker-compose"]
|
||||||
|
aliases = ["/compose/reference/docker-compose/"]
|
||||||
[menu.main]
|
[menu.main]
|
||||||
parent = "smn_compose_cli"
|
parent = "smn_compose_cli"
|
||||||
weight=-2
|
weight=-2
|
||||||
|
|
@ -10,80 +11,107 @@ weight=-2
|
||||||
<![end-metadata]-->
|
<![end-metadata]-->
|
||||||
|
|
||||||
|
|
||||||
# Introduction to the CLI
|
# Overview of docker-compose CLI
|
||||||
|
|
||||||
This section describes the subcommands you can use with the `docker-compose` command. You can run subcommand against one or more services. To run against a specific service, you supply the service name from your compose configuration. If you do not specify the service name, the command runs against all the services in your configuration.
|
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)
|
||||||
|
|
||||||
|
|
||||||
## Commands
|
## Where to go next
|
||||||
|
|
||||||
* [docker-compose Command](docker-compose.md)
|
* [CLI environment variables](envvars.md)
|
||||||
* [CLI Reference](index.md)
|
|
||||||
|
|
||||||
|
|
||||||
## 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](docker-compose.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](docker-compose.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)
|
|
||||||
|
|
|
||||||
184
docs/swarm.md
Normal file
184
docs/swarm.md
Normal 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).
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
<!--[metadata]>
|
<!--[metadata]>
|
||||||
+++
|
+++
|
||||||
title = "Quickstart Guide: Compose and WordPress"
|
title = "Quickstart: Compose and WordPress"
|
||||||
description = "Getting started with Compose and WordPress"
|
description = "Getting started with Compose and WordPress"
|
||||||
keywords = ["documentation, docs, docker, compose, orchestration, containers"]
|
keywords = ["documentation, docs, docker, compose, orchestration, containers"]
|
||||||
[menu.main]
|
[menu.main]
|
||||||
parent="smn_workw_compose"
|
parent="workw_compose"
|
||||||
weight=6
|
weight=6
|
||||||
+++
|
+++
|
||||||
<![end-metadata]-->
|
<![end-metadata]-->
|
||||||
|
|
||||||
|
|
||||||
# Quickstart Guide: Compose and WordPress
|
# Quickstart: Compose and WordPress
|
||||||
|
|
||||||
You can use Compose to easily run WordPress in an isolated environment built
|
You can use Compose to easily run WordPress in an isolated environment built
|
||||||
with Docker containers.
|
with Docker containers.
|
||||||
|
|
@ -41,19 +41,21 @@ and WordPress.
|
||||||
Next you'll create a `docker-compose.yml` file that will start your web service
|
Next you'll create a `docker-compose.yml` file that will start your web service
|
||||||
and a separate MySQL instance:
|
and a separate MySQL instance:
|
||||||
|
|
||||||
web:
|
version: '2'
|
||||||
build: .
|
services:
|
||||||
command: php -S 0.0.0.0:8000 -t /code
|
web:
|
||||||
ports:
|
build: .
|
||||||
- "8000:8000"
|
command: php -S 0.0.0.0:8000 -t /code
|
||||||
links:
|
ports:
|
||||||
- db
|
- "8000:8000"
|
||||||
volumes:
|
depends_on:
|
||||||
- .:/code
|
- db
|
||||||
db:
|
volumes:
|
||||||
image: orchardup/mysql
|
- .:/code
|
||||||
environment:
|
db:
|
||||||
MYSQL_DATABASE: wordpress
|
image: orchardup/mysql
|
||||||
|
environment:
|
||||||
|
MYSQL_DATABASE: wordpress
|
||||||
|
|
||||||
A supporting file is needed to get this working. `wp-config.php` is
|
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
|
the standard WordPress config file with a single change to point the database
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
pyinstaller==3.0
|
pyinstaller==3.1.1
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
PyYAML==3.11
|
PyYAML==3.11
|
||||||
cached-property==1.2.0
|
cached-property==1.2.0
|
||||||
dockerpty==0.3.4
|
docker-py==1.7.1
|
||||||
|
dockerpty==0.4.1
|
||||||
docopt==0.6.1
|
docopt==0.6.1
|
||||||
enum34==1.0.4
|
enum34==1.0.4
|
||||||
git+https://github.com/docker/docker-py.git@master#egg=docker-py
|
|
||||||
jsonschema==2.5.1
|
jsonschema==2.5.1
|
||||||
requests==2.7.0
|
requests==2.7.0
|
||||||
six==1.7.3
|
six==1.7.3
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,9 @@ Get-ChildItem -Recurse -Include *.pyc | foreach ($_) { Remove-Item $_.FullName }
|
||||||
# Create virtualenv
|
# Create virtualenv
|
||||||
virtualenv .\venv
|
virtualenv .\venv
|
||||||
|
|
||||||
|
# pip and pyinstaller generate lots of warnings, so we need to ignore them
|
||||||
|
$ErrorActionPreference = "Continue"
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
.\venv\Scripts\pip install pypiwin32==219
|
.\venv\Scripts\pip install pypiwin32==219
|
||||||
.\venv\Scripts\pip install -r requirements.txt
|
.\venv\Scripts\pip install -r requirements.txt
|
||||||
|
|
@ -50,8 +53,6 @@ virtualenv .\venv
|
||||||
git rev-parse --short HEAD | out-file -encoding ASCII compose\GITSHA
|
git rev-parse --short HEAD | out-file -encoding ASCII compose\GITSHA
|
||||||
|
|
||||||
# Build binary
|
# Build binary
|
||||||
# pyinstaller has lots of warnings, so we need to run with ErrorAction = Continue
|
|
||||||
$ErrorActionPreference = "Continue"
|
|
||||||
.\venv\Scripts\pyinstaller .\docker-compose.spec
|
.\venv\Scripts\pyinstaller .\docker-compose.spec
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,13 @@ PREV_RELEASE=$1
|
||||||
VERSION=HEAD
|
VERSION=HEAD
|
||||||
URL="https://api.github.com/repos/docker/compose/compare"
|
URL="https://api.github.com/repos/docker/compose/compare"
|
||||||
|
|
||||||
curl -sf "$URL/$PREV_RELEASE...$VERSION" | \
|
contribs=$(curl -sf "$URL/$PREV_RELEASE...$VERSION" | \
|
||||||
jq -r '.commits[].author.login' | \
|
jq -r '.commits[].author.login' | \
|
||||||
sort | \
|
sort | \
|
||||||
uniq -c | \
|
uniq -c | \
|
||||||
sort -nr | \
|
sort -nr)
|
||||||
awk '{print "@"$2","}' | \
|
|
||||||
xargs echo
|
echo "Contributions by user: "
|
||||||
|
echo "$contribs"
|
||||||
|
echo
|
||||||
|
echo "$contribs" | awk '{print "@"$2","}' | xargs
|
||||||
|
|
|
||||||
|
|
@ -63,15 +63,17 @@ git merge --strategy=ours --no-edit $REMOTE/release
|
||||||
git config "branch.${BRANCH}.release" $VERSION
|
git config "branch.${BRANCH}.release" $VERSION
|
||||||
|
|
||||||
|
|
||||||
|
editor=${EDITOR:-vim}
|
||||||
|
|
||||||
echo "Update versions in docs/install.md, compose/__init__.py, script/run.sh"
|
echo "Update versions in docs/install.md, compose/__init__.py, script/run.sh"
|
||||||
$EDITOR docs/install.md
|
$editor docs/install.md
|
||||||
$EDITOR compose/__init__.py
|
$editor compose/__init__.py
|
||||||
$EDITOR script/run.sh
|
$editor script/run.sh
|
||||||
|
|
||||||
|
|
||||||
echo "Write release notes in CHANGELOG.md"
|
echo "Write release notes in CHANGELOG.md"
|
||||||
browser "https://github.com/docker/compose/issues?q=milestone%3A$VERSION+is%3Aclosed"
|
browser "https://github.com/docker/compose/issues?q=milestone%3A$VERSION+is%3Aclosed"
|
||||||
$EDITOR CHANGELOG.md
|
$editor CHANGELOG.md
|
||||||
|
|
||||||
|
|
||||||
git diff
|
git diff
|
||||||
|
|
@ -84,10 +86,10 @@ echo "Push branch to user remote"
|
||||||
GITHUB_USER=$USER
|
GITHUB_USER=$USER
|
||||||
USER_REMOTE="$(find_remote $GITHUB_USER/compose)"
|
USER_REMOTE="$(find_remote $GITHUB_USER/compose)"
|
||||||
if [ -z "$USER_REMOTE" ]; then
|
if [ -z "$USER_REMOTE" ]; then
|
||||||
echo "No user remote found for $GITHUB_USER"
|
echo "$GITHUB_USER/compose not found"
|
||||||
read -r -p "Enter the name of your github user: " GITHUB_USER
|
read -r -p "Enter the name of your GitHub fork (username/repo): " GITHUB_REPO
|
||||||
# assumes there is already a user remote somewhere
|
# assumes there is already a user remote somewhere
|
||||||
USER_REMOTE=$(find_remote $GITHUB_USER/compose)
|
USER_REMOTE=$(find_remote $GITHUB_REPO)
|
||||||
fi
|
fi
|
||||||
if [ -z "$USER_REMOTE" ]; then
|
if [ -z "$USER_REMOTE" ]; then
|
||||||
>&2 echo "No user remote found. You need to 'git push' your branch."
|
>&2 echo "No user remote found. You need to 'git push' your branch."
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ sed -i -e 's/logo.png?raw=true/https:\/\/github.com\/docker\/compose\/raw\/maste
|
||||||
./script/write-git-sha
|
./script/write-git-sha
|
||||||
python setup.py sdist
|
python setup.py sdist
|
||||||
if [ "$(command -v twine 2> /dev/null)" ]; then
|
if [ "$(command -v twine 2> /dev/null)" ]; then
|
||||||
twine upload ./dist/docker-compose-${VERSION}.tar.gz
|
twine upload ./dist/docker-compose-${VERSION/-/}.tar.gz
|
||||||
else
|
else
|
||||||
python setup.py upload
|
python setup.py upload
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ if [[ "$sha" == "$(git rev-parse HEAD)" ]]; then
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
commits=$(git log --format="%H" "$sha..HEAD" | wc -l)
|
commits=$(git log --format="%H" "$sha..HEAD" | wc -l | xargs echo)
|
||||||
|
|
||||||
git rebase --onto $sha~1 HEAD~$commits $BRANCH
|
git rebase --onto $sha~1 HEAD~$commits $BRANCH
|
||||||
git cherry-pick $sha
|
git cherry-pick $sha
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
# $Env:DOCKER_COMPOSE_OPTIONS.
|
# $Env:DOCKER_COMPOSE_OPTIONS.
|
||||||
|
|
||||||
if ($Env:DOCKER_COMPOSE_VERSION -eq $null -or $Env:DOCKER_COMPOSE_VERSION.Length -eq 0) {
|
if ($Env:DOCKER_COMPOSE_VERSION -eq $null -or $Env:DOCKER_COMPOSE_VERSION.Length -eq 0) {
|
||||||
$Env:DOCKER_COMPOSE_VERSION = "latest"
|
$Env:DOCKER_COMPOSE_VERSION = "1.6.0rc1"
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($Env:DOCKER_COMPOSE_OPTIONS -eq $null) {
|
if ($Env:DOCKER_COMPOSE_OPTIONS -eq $null) {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
VERSION="1.5.2"
|
VERSION="1.6.1"
|
||||||
IMAGE="docker/compose:$VERSION"
|
IMAGE="docker/compose:$VERSION"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -31,7 +31,9 @@ fi
|
||||||
|
|
||||||
|
|
||||||
# Setup volume mounts for compose config and context
|
# Setup volume mounts for compose config and context
|
||||||
VOLUMES="-v $(pwd):$(pwd)"
|
if [ "$(pwd)" != '/' ]; then
|
||||||
|
VOLUMES="-v $(pwd):$(pwd)"
|
||||||
|
fi
|
||||||
if [ -n "$COMPOSE_FILE" ]; then
|
if [ -n "$COMPOSE_FILE" ]; then
|
||||||
compose_dir=$(dirname $COMPOSE_FILE)
|
compose_dir=$(dirname $COMPOSE_FILE)
|
||||||
fi
|
fi
|
||||||
|
|
@ -45,9 +47,10 @@ fi
|
||||||
|
|
||||||
# Only allocate tty if we detect one
|
# Only allocate tty if we detect one
|
||||||
if [ -t 1 ]; then
|
if [ -t 1 ]; then
|
||||||
DOCKER_RUN_OPTIONS="-ti"
|
DOCKER_RUN_OPTIONS="-t"
|
||||||
else
|
fi
|
||||||
DOCKER_RUN_OPTIONS="-i"
|
if [ -t 0 ]; then
|
||||||
|
DOCKER_RUN_OPTIONS="$DOCKER_RUN_OPTIONS -i"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exec docker run --rm $DOCKER_RUN_OPTIONS $DOCKER_ADDR $COMPOSE_OPTIONS $VOLUMES -w $(pwd) $IMAGE $@
|
exec docker run --rm $DOCKER_RUN_OPTIONS $DOCKER_ADDR $COMPOSE_OPTIONS $VOLUMES -w "$(pwd)" $IMAGE "$@"
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ get_versions="docker run --rm
|
||||||
if [ "$DOCKER_VERSIONS" == "" ]; then
|
if [ "$DOCKER_VERSIONS" == "" ]; then
|
||||||
DOCKER_VERSIONS="$($get_versions default)"
|
DOCKER_VERSIONS="$($get_versions default)"
|
||||||
elif [ "$DOCKER_VERSIONS" == "all" ]; then
|
elif [ "$DOCKER_VERSIONS" == "all" ]; then
|
||||||
DOCKER_VERSIONS="1.9.1 1.10.0-dev"
|
DOCKER_VERSIONS=$($get_versions -n 2 recent)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -38,11 +38,7 @@ for version in $DOCKER_VERSIONS; do
|
||||||
|
|
||||||
trap "on_exit" EXIT
|
trap "on_exit" EXIT
|
||||||
|
|
||||||
if [[ $version == *"-dev" ]]; then
|
repo="dockerswarm/dind"
|
||||||
repo="dnephin/dind"
|
|
||||||
else
|
|
||||||
repo="dockerswarm/dind"
|
|
||||||
fi
|
|
||||||
|
|
||||||
docker run \
|
docker run \
|
||||||
-d \
|
-d \
|
||||||
|
|
|
||||||
4
setup.py
4
setup.py
|
|
@ -34,8 +34,8 @@ install_requires = [
|
||||||
'requests >= 2.6.1, < 2.8',
|
'requests >= 2.6.1, < 2.8',
|
||||||
'texttable >= 0.8.1, < 0.9',
|
'texttable >= 0.8.1, < 0.9',
|
||||||
'websocket-client >= 0.32.0, < 1.0',
|
'websocket-client >= 0.32.0, < 1.0',
|
||||||
'docker-py >= 1.5.0, < 2',
|
'docker-py >= 1.7.0, < 2',
|
||||||
'dockerpty >= 0.3.4, < 0.4',
|
'dockerpty >= 0.4.1, < 0.5',
|
||||||
'six >= 1.3.0, < 2',
|
'six >= 1.3.0, < 2',
|
||||||
'jsonschema >= 2.5.1, < 3',
|
'jsonschema >= 2.5.1, < 3',
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -43,12 +43,13 @@ def start_process(base_dir, options):
|
||||||
def wait_on_process(proc, returncode=0):
|
def wait_on_process(proc, returncode=0):
|
||||||
stdout, stderr = proc.communicate()
|
stdout, stderr = proc.communicate()
|
||||||
if proc.returncode != returncode:
|
if proc.returncode != returncode:
|
||||||
print(stderr.decode('utf-8'))
|
print("Stderr: {}".format(stderr))
|
||||||
|
print("Stdout: {}".format(stdout))
|
||||||
assert proc.returncode == returncode
|
assert proc.returncode == returncode
|
||||||
return ProcessResult(stdout.decode('utf-8'), stderr.decode('utf-8'))
|
return ProcessResult(stdout.decode('utf-8'), stderr.decode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
def wait_on_condition(condition, delay=0.1, timeout=20):
|
def wait_on_condition(condition, delay=0.1, timeout=40):
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
while not condition():
|
while not condition():
|
||||||
if time.time() - start_time > timeout:
|
if time.time() - start_time > timeout:
|
||||||
|
|
@ -81,7 +82,6 @@ class ContainerStateCondition(object):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.running = running
|
self.running = running
|
||||||
|
|
||||||
# State.Running == true
|
|
||||||
def __call__(self):
|
def __call__(self):
|
||||||
try:
|
try:
|
||||||
container = self.client.inspect_container(self.name)
|
container = self.client.inspect_container(self.name)
|
||||||
|
|
@ -159,7 +159,7 @@ class CLITestCase(DockerClientTestCase):
|
||||||
'-f', 'tests/fixtures/invalid-composefile/invalid.yml',
|
'-f', 'tests/fixtures/invalid-composefile/invalid.yml',
|
||||||
'config', '-q'
|
'config', '-q'
|
||||||
], returncode=1)
|
], returncode=1)
|
||||||
assert "'notaservice' doesn't have any configuration" in result.stderr
|
assert "'notaservice' must be a mapping" in result.stderr
|
||||||
|
|
||||||
# TODO: this shouldn't be v2-dependent
|
# TODO: this shouldn't be v2-dependent
|
||||||
@v2_only()
|
@v2_only()
|
||||||
|
|
@ -177,7 +177,7 @@ class CLITestCase(DockerClientTestCase):
|
||||||
|
|
||||||
output = yaml.load(result.stdout)
|
output = yaml.load(result.stdout)
|
||||||
expected = {
|
expected = {
|
||||||
'version': 2,
|
'version': '2.0',
|
||||||
'volumes': {'data': {'driver': 'local'}},
|
'volumes': {'data': {'driver': 'local'}},
|
||||||
'networks': {'front': {}},
|
'networks': {'front': {}},
|
||||||
'services': {
|
'services': {
|
||||||
|
|
@ -185,7 +185,7 @@ class CLITestCase(DockerClientTestCase):
|
||||||
'build': {
|
'build': {
|
||||||
'context': os.path.abspath(self.base_dir),
|
'context': os.path.abspath(self.base_dir),
|
||||||
},
|
},
|
||||||
'networks': ['front', 'default'],
|
'networks': {'front': None, 'default': None},
|
||||||
'volumes_from': ['service:other:rw'],
|
'volumes_from': ['service:other:rw'],
|
||||||
},
|
},
|
||||||
'other': {
|
'other': {
|
||||||
|
|
@ -406,9 +406,11 @@ class CLITestCase(DockerClientTestCase):
|
||||||
|
|
||||||
services = self.project.get_services()
|
services = self.project.get_services()
|
||||||
|
|
||||||
networks = self.client.networks(names=[self.project.default_network.full_name])
|
network_name = self.project.networks.networks['default'].full_name
|
||||||
|
networks = self.client.networks(names=[network_name])
|
||||||
self.assertEqual(len(networks), 1)
|
self.assertEqual(len(networks), 1)
|
||||||
self.assertEqual(networks[0]['Driver'], 'bridge')
|
self.assertEqual(networks[0]['Driver'], 'bridge')
|
||||||
|
assert 'com.docker.network.bridge.enable_icc' not in networks[0]['Options']
|
||||||
|
|
||||||
network = self.client.inspect_network(networks[0]['Id'])
|
network = self.client.inspect_network(networks[0]['Id'])
|
||||||
|
|
||||||
|
|
@ -419,12 +421,58 @@ class CLITestCase(DockerClientTestCase):
|
||||||
container = containers[0]
|
container = containers[0]
|
||||||
self.assertIn(container.id, network['Containers'])
|
self.assertIn(container.id, network['Containers'])
|
||||||
|
|
||||||
networks = list(container.get('NetworkSettings.Networks'))
|
networks = container.get('NetworkSettings.Networks')
|
||||||
self.assertEqual(networks, [network['Name']])
|
self.assertEqual(list(networks), [network['Name']])
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
sorted(networks[network['Name']]['Aliases']),
|
||||||
|
sorted([service.name, container.short_id]))
|
||||||
|
|
||||||
for service in services:
|
for service in services:
|
||||||
assert self.lookup(container, service.name)
|
assert self.lookup(container, service.name)
|
||||||
|
|
||||||
|
@v2_only()
|
||||||
|
def test_up_with_default_network_config(self):
|
||||||
|
filename = 'default-network-config.yml'
|
||||||
|
|
||||||
|
self.base_dir = 'tests/fixtures/networks'
|
||||||
|
self._project = get_project(self.base_dir, [filename])
|
||||||
|
|
||||||
|
self.dispatch(['-f', filename, 'up', '-d'], None)
|
||||||
|
|
||||||
|
network_name = self.project.networks.networks['default'].full_name
|
||||||
|
networks = self.client.networks(names=[network_name])
|
||||||
|
|
||||||
|
assert networks[0]['Options']['com.docker.network.bridge.enable_icc'] == 'false'
|
||||||
|
|
||||||
|
@v2_only()
|
||||||
|
def test_up_with_network_aliases(self):
|
||||||
|
filename = 'network-aliases.yml'
|
||||||
|
self.base_dir = 'tests/fixtures/networks'
|
||||||
|
self.dispatch(['-f', filename, 'up', '-d'], None)
|
||||||
|
back_name = '{}_back'.format(self.project.name)
|
||||||
|
front_name = '{}_front'.format(self.project.name)
|
||||||
|
|
||||||
|
networks = [
|
||||||
|
n for n in self.client.networks()
|
||||||
|
if n['Name'].startswith('{}_'.format(self.project.name))
|
||||||
|
]
|
||||||
|
|
||||||
|
# Two networks were created: back and front
|
||||||
|
assert sorted(n['Name'] for n in networks) == [back_name, front_name]
|
||||||
|
web_container = self.project.get_service('web').containers()[0]
|
||||||
|
|
||||||
|
back_aliases = web_container.get(
|
||||||
|
'NetworkSettings.Networks.{}.Aliases'.format(back_name)
|
||||||
|
)
|
||||||
|
assert 'web' in back_aliases
|
||||||
|
front_aliases = web_container.get(
|
||||||
|
'NetworkSettings.Networks.{}.Aliases'.format(front_name)
|
||||||
|
)
|
||||||
|
assert 'web' in front_aliases
|
||||||
|
assert 'forward_facing' in front_aliases
|
||||||
|
assert 'ahead' in front_aliases
|
||||||
|
|
||||||
@v2_only()
|
@v2_only()
|
||||||
def test_up_with_networks(self):
|
def test_up_with_networks(self):
|
||||||
self.base_dir = 'tests/fixtures/networks'
|
self.base_dir = 'tests/fixtures/networks'
|
||||||
|
|
@ -448,6 +496,10 @@ class CLITestCase(DockerClientTestCase):
|
||||||
app_container = self.project.get_service('app').containers()[0]
|
app_container = self.project.get_service('app').containers()[0]
|
||||||
db_container = self.project.get_service('db').containers()[0]
|
db_container = self.project.get_service('db').containers()[0]
|
||||||
|
|
||||||
|
for net_name in [front_name, back_name]:
|
||||||
|
links = app_container.get('NetworkSettings.Networks.{}.Links'.format(net_name))
|
||||||
|
assert '{}:database'.format(db_container.name) in links
|
||||||
|
|
||||||
# db and app joined the back network
|
# db and app joined the back network
|
||||||
assert sorted(back_network['Containers']) == sorted([db_container.id, app_container.id])
|
assert sorted(back_network['Containers']) == sorted([db_container.id, app_container.id])
|
||||||
|
|
||||||
|
|
@ -461,6 +513,9 @@ class CLITestCase(DockerClientTestCase):
|
||||||
# app can see db
|
# app can see db
|
||||||
assert self.lookup(app_container, "db")
|
assert self.lookup(app_container, "db")
|
||||||
|
|
||||||
|
# app has aliased db to "database"
|
||||||
|
assert self.lookup(app_container, "database")
|
||||||
|
|
||||||
@v2_only()
|
@v2_only()
|
||||||
def test_up_missing_network(self):
|
def test_up_missing_network(self):
|
||||||
self.base_dir = 'tests/fixtures/networks'
|
self.base_dir = 'tests/fixtures/networks'
|
||||||
|
|
@ -472,8 +527,13 @@ class CLITestCase(DockerClientTestCase):
|
||||||
assert 'Service "web" uses an undefined network "foo"' in result.stderr
|
assert 'Service "web" uses an undefined network "foo"' in result.stderr
|
||||||
|
|
||||||
@v2_only()
|
@v2_only()
|
||||||
def test_up_predefined_networks(self):
|
def test_up_with_network_mode(self):
|
||||||
filename = 'predefined-networks.yml'
|
c = self.client.create_container('busybox', 'top', name='composetest_network_mode_container')
|
||||||
|
self.addCleanup(self.client.remove_container, c, force=True)
|
||||||
|
self.client.start(c)
|
||||||
|
container_mode_source = 'container:{}'.format(c['Id'])
|
||||||
|
|
||||||
|
filename = 'network-mode.yml'
|
||||||
|
|
||||||
self.base_dir = 'tests/fixtures/networks'
|
self.base_dir = 'tests/fixtures/networks'
|
||||||
self._project = get_project(self.base_dir, [filename])
|
self._project = get_project(self.base_dir, [filename])
|
||||||
|
|
@ -491,6 +551,16 @@ class CLITestCase(DockerClientTestCase):
|
||||||
assert list(container.get('NetworkSettings.Networks')) == [name]
|
assert list(container.get('NetworkSettings.Networks')) == [name]
|
||||||
assert container.get('HostConfig.NetworkMode') == name
|
assert container.get('HostConfig.NetworkMode') == name
|
||||||
|
|
||||||
|
service_mode_source = 'container:{}'.format(
|
||||||
|
self.project.get_service('bridge').containers()[0].id)
|
||||||
|
service_mode_container = self.project.get_service('service').containers()[0]
|
||||||
|
assert not service_mode_container.get('NetworkSettings.Networks')
|
||||||
|
assert service_mode_container.get('HostConfig.NetworkMode') == service_mode_source
|
||||||
|
|
||||||
|
container_mode_container = self.project.get_service('container').containers()[0]
|
||||||
|
assert not container_mode_container.get('NetworkSettings.Networks')
|
||||||
|
assert container_mode_container.get('HostConfig.NetworkMode') == container_mode_source
|
||||||
|
|
||||||
@v2_only()
|
@v2_only()
|
||||||
def test_up_external_networks(self):
|
def test_up_external_networks(self):
|
||||||
filename = 'external-networks.yml'
|
filename = 'external-networks.yml'
|
||||||
|
|
@ -515,6 +585,29 @@ class CLITestCase(DockerClientTestCase):
|
||||||
container = self.project.containers()[0]
|
container = self.project.containers()[0]
|
||||||
assert sorted(list(container.get('NetworkSettings.Networks'))) == sorted(network_names)
|
assert sorted(list(container.get('NetworkSettings.Networks'))) == sorted(network_names)
|
||||||
|
|
||||||
|
@v2_only()
|
||||||
|
def test_up_with_external_default_network(self):
|
||||||
|
filename = 'external-default.yml'
|
||||||
|
|
||||||
|
self.base_dir = 'tests/fixtures/networks'
|
||||||
|
self._project = get_project(self.base_dir, [filename])
|
||||||
|
|
||||||
|
result = self.dispatch(['-f', filename, 'up', '-d'], returncode=1)
|
||||||
|
assert 'declared as external, but could not be found' in result.stderr
|
||||||
|
|
||||||
|
networks = [
|
||||||
|
n['Name'] for n in self.client.networks()
|
||||||
|
if n['Name'].startswith('{}_'.format(self.project.name))
|
||||||
|
]
|
||||||
|
assert not networks
|
||||||
|
|
||||||
|
network_name = 'composetest_external_network'
|
||||||
|
self.client.create_network(network_name)
|
||||||
|
|
||||||
|
self.dispatch(['-f', filename, 'up', '-d'])
|
||||||
|
container = self.project.containers()[0]
|
||||||
|
assert list(container.get('NetworkSettings.Networks')) == [network_name]
|
||||||
|
|
||||||
@v2_only()
|
@v2_only()
|
||||||
def test_up_no_services(self):
|
def test_up_no_services(self):
|
||||||
self.base_dir = 'tests/fixtures/no-services'
|
self.base_dir = 'tests/fixtures/no-services'
|
||||||
|
|
@ -524,30 +617,15 @@ class CLITestCase(DockerClientTestCase):
|
||||||
n['Name'] for n in self.client.networks()
|
n['Name'] for n in self.client.networks()
|
||||||
if n['Name'].startswith('{}_'.format(self.project.name))
|
if n['Name'].startswith('{}_'.format(self.project.name))
|
||||||
]
|
]
|
||||||
|
assert network_names == []
|
||||||
assert sorted(network_names) == [
|
|
||||||
'{}_{}'.format(self.project.name, name)
|
|
||||||
for name in ['bar', 'foo']
|
|
||||||
]
|
|
||||||
|
|
||||||
@v2_only()
|
|
||||||
def test_up_with_links_is_invalid(self):
|
|
||||||
self.base_dir = 'tests/fixtures/v2-simple'
|
|
||||||
|
|
||||||
result = self.dispatch(
|
|
||||||
['-f', 'links-invalid.yml', 'up', '-d'],
|
|
||||||
returncode=1)
|
|
||||||
|
|
||||||
# TODO: fix validation error messages for v2 files
|
|
||||||
# assert "Unsupported config option for service 'simple': 'links'" in result.stderr
|
|
||||||
assert "Unsupported config option" in result.stderr
|
|
||||||
|
|
||||||
def test_up_with_links_v1(self):
|
def test_up_with_links_v1(self):
|
||||||
self.base_dir = 'tests/fixtures/links-composefile'
|
self.base_dir = 'tests/fixtures/links-composefile'
|
||||||
self.dispatch(['up', '-d', 'web'], None)
|
self.dispatch(['up', '-d', 'web'], None)
|
||||||
|
|
||||||
# No network was created
|
# No network was created
|
||||||
networks = self.client.networks(names=[self.project.default_network.full_name])
|
network_name = self.project.networks.networks['default'].full_name
|
||||||
|
networks = self.client.networks(names=[network_name])
|
||||||
assert networks == []
|
assert networks == []
|
||||||
|
|
||||||
web = self.project.get_service('web')
|
web = self.project.get_service('web')
|
||||||
|
|
@ -648,14 +726,31 @@ class CLITestCase(DockerClientTestCase):
|
||||||
wait_on_condition(ContainerCountCondition(self.project, 2))
|
wait_on_condition(ContainerCountCondition(self.project, 2))
|
||||||
|
|
||||||
os.kill(proc.pid, signal.SIGINT)
|
os.kill(proc.pid, signal.SIGINT)
|
||||||
wait_on_condition(ContainerCountCondition(self.project, 0), timeout=30)
|
wait_on_condition(ContainerCountCondition(self.project, 0))
|
||||||
|
|
||||||
def test_up_handles_sigterm(self):
|
def test_up_handles_sigterm(self):
|
||||||
proc = start_process(self.base_dir, ['up', '-t', '2'])
|
proc = start_process(self.base_dir, ['up', '-t', '2'])
|
||||||
wait_on_condition(ContainerCountCondition(self.project, 2))
|
wait_on_condition(ContainerCountCondition(self.project, 2))
|
||||||
|
|
||||||
os.kill(proc.pid, signal.SIGTERM)
|
os.kill(proc.pid, signal.SIGTERM)
|
||||||
wait_on_condition(ContainerCountCondition(self.project, 0), timeout=30)
|
wait_on_condition(ContainerCountCondition(self.project, 0))
|
||||||
|
|
||||||
|
@v2_only()
|
||||||
|
def test_up_handles_force_shutdown(self):
|
||||||
|
self.base_dir = 'tests/fixtures/sleeps-composefile'
|
||||||
|
proc = start_process(self.base_dir, ['up', '-t', '200'])
|
||||||
|
wait_on_condition(ContainerCountCondition(self.project, 2))
|
||||||
|
|
||||||
|
os.kill(proc.pid, signal.SIGTERM)
|
||||||
|
time.sleep(0.1)
|
||||||
|
os.kill(proc.pid, signal.SIGTERM)
|
||||||
|
wait_on_condition(ContainerCountCondition(self.project, 0))
|
||||||
|
|
||||||
|
def test_up_handles_abort_on_container_exit(self):
|
||||||
|
start_process(self.base_dir, ['up', '--abort-on-container-exit'])
|
||||||
|
wait_on_condition(ContainerCountCondition(self.project, 2))
|
||||||
|
self.project.stop(['simple'])
|
||||||
|
wait_on_condition(ContainerCountCondition(self.project, 0))
|
||||||
|
|
||||||
def test_run_service_without_links(self):
|
def test_run_service_without_links(self):
|
||||||
self.base_dir = 'tests/fixtures/links-composefile'
|
self.base_dir = 'tests/fixtures/links-composefile'
|
||||||
|
|
@ -677,6 +772,15 @@ class CLITestCase(DockerClientTestCase):
|
||||||
self.assertEqual(len(db.containers()), 1)
|
self.assertEqual(len(db.containers()), 1)
|
||||||
self.assertEqual(len(console.containers()), 0)
|
self.assertEqual(len(console.containers()), 0)
|
||||||
|
|
||||||
|
@v2_only()
|
||||||
|
def test_run_service_with_dependencies(self):
|
||||||
|
self.base_dir = 'tests/fixtures/v2-dependencies'
|
||||||
|
self.dispatch(['run', 'web', '/bin/true'], None)
|
||||||
|
db = self.project.get_service('db')
|
||||||
|
console = self.project.get_service('console')
|
||||||
|
self.assertEqual(len(db.containers()), 1)
|
||||||
|
self.assertEqual(len(console.containers()), 0)
|
||||||
|
|
||||||
def test_run_with_no_deps(self):
|
def test_run_with_no_deps(self):
|
||||||
self.base_dir = 'tests/fixtures/links-composefile'
|
self.base_dir = 'tests/fixtures/links-composefile'
|
||||||
self.dispatch(['run', '--no-deps', 'web', '/bin/true'])
|
self.dispatch(['run', '--no-deps', 'web', '/bin/true'])
|
||||||
|
|
@ -872,14 +976,47 @@ class CLITestCase(DockerClientTestCase):
|
||||||
self.assertEqual(container.name, name)
|
self.assertEqual(container.name, name)
|
||||||
|
|
||||||
@v2_only()
|
@v2_only()
|
||||||
def test_run_with_networking(self):
|
def test_run_interactive_connects_to_network(self):
|
||||||
self.base_dir = 'tests/fixtures/v2-simple'
|
self.base_dir = 'tests/fixtures/networks'
|
||||||
self.dispatch(['run', 'simple', 'true'], None)
|
|
||||||
service = self.project.get_service('simple')
|
self.dispatch(['up', '-d'])
|
||||||
container, = service.containers(stopped=True, one_off=True)
|
self.dispatch(['run', 'app', 'nslookup', 'app'])
|
||||||
networks = self.client.networks(names=[self.project.default_network.full_name])
|
self.dispatch(['run', 'app', 'nslookup', 'db'])
|
||||||
self.assertEqual(len(networks), 1)
|
|
||||||
self.assertEqual(container.human_readable_command, u'true')
|
containers = self.project.get_service('app').containers(
|
||||||
|
stopped=True, one_off=True)
|
||||||
|
assert len(containers) == 2
|
||||||
|
|
||||||
|
for container in containers:
|
||||||
|
networks = container.get('NetworkSettings.Networks')
|
||||||
|
|
||||||
|
assert sorted(list(networks)) == [
|
||||||
|
'{}_{}'.format(self.project.name, name)
|
||||||
|
for name in ['back', 'front']
|
||||||
|
]
|
||||||
|
|
||||||
|
for _, config in networks.items():
|
||||||
|
assert not config['Aliases']
|
||||||
|
|
||||||
|
@v2_only()
|
||||||
|
def test_run_detached_connects_to_network(self):
|
||||||
|
self.base_dir = 'tests/fixtures/networks'
|
||||||
|
self.dispatch(['up', '-d'])
|
||||||
|
self.dispatch(['run', '-d', 'app', 'top'])
|
||||||
|
|
||||||
|
container = self.project.get_service('app').containers(one_off=True)[0]
|
||||||
|
networks = container.get('NetworkSettings.Networks')
|
||||||
|
|
||||||
|
assert sorted(list(networks)) == [
|
||||||
|
'{}_{}'.format(self.project.name, name)
|
||||||
|
for name in ['back', 'front']
|
||||||
|
]
|
||||||
|
|
||||||
|
for _, config in networks.items():
|
||||||
|
assert not config['Aliases']
|
||||||
|
|
||||||
|
assert self.lookup(container, 'app')
|
||||||
|
assert self.lookup(container, 'db')
|
||||||
|
|
||||||
def test_run_handles_sigint(self):
|
def test_run_handles_sigint(self):
|
||||||
proc = start_process(self.base_dir, ['run', '-T', 'simple', 'top'])
|
proc = start_process(self.base_dir, ['run', '-T', 'simple', 'top'])
|
||||||
|
|
|
||||||
1
tests/fixtures/extends/common.yml
vendored
1
tests/fixtures/extends/common.yml
vendored
|
|
@ -1,6 +1,7 @@
|
||||||
web:
|
web:
|
||||||
image: busybox
|
image: busybox
|
||||||
command: /bin/true
|
command: /bin/true
|
||||||
|
net: host
|
||||||
environment:
|
environment:
|
||||||
- FOO=1
|
- FOO=1
|
||||||
- BAR=1
|
- BAR=1
|
||||||
|
|
|
||||||
1
tests/fixtures/extends/docker-compose.yml
vendored
1
tests/fixtures/extends/docker-compose.yml
vendored
|
|
@ -11,6 +11,7 @@ myweb:
|
||||||
BAR: "2"
|
BAR: "2"
|
||||||
# add BAZ
|
# add BAZ
|
||||||
BAZ: "2"
|
BAZ: "2"
|
||||||
|
net: bridge
|
||||||
mydb:
|
mydb:
|
||||||
image: busybox
|
image: busybox
|
||||||
command: top
|
command: top
|
||||||
|
|
|
||||||
12
tests/fixtures/extends/invalid-net-v2.yml
vendored
Normal file
12
tests/fixtures/extends/invalid-net-v2.yml
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
version: "2"
|
||||||
|
services:
|
||||||
|
myweb:
|
||||||
|
build: '.'
|
||||||
|
extends:
|
||||||
|
service: web
|
||||||
|
command: top
|
||||||
|
web:
|
||||||
|
build: '.'
|
||||||
|
network_mode: "service:net"
|
||||||
|
net:
|
||||||
|
build: '.'
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
version: 2
|
version: "2"
|
||||||
services:
|
services:
|
||||||
simple:
|
simple:
|
||||||
image: busybox:latest
|
image: busybox:latest
|
||||||
|
|
|
||||||
2
tests/fixtures/net-container/v2-invalid.yml
vendored
2
tests/fixtures/net-container/v2-invalid.yml
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
version: 2
|
version: "2"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
foo:
|
foo:
|
||||||
|
|
|
||||||
9
tests/fixtures/networks/bridge.yml
vendored
Normal file
9
tests/fixtures/networks/bridge.yml
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
version: "2"
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: busybox
|
||||||
|
command: top
|
||||||
|
networks:
|
||||||
|
- bridge
|
||||||
|
- default
|
||||||
13
tests/fixtures/networks/default-network-config.yml
vendored
Normal file
13
tests/fixtures/networks/default-network-config.yml
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
version: "2"
|
||||||
|
services:
|
||||||
|
simple:
|
||||||
|
image: busybox:latest
|
||||||
|
command: top
|
||||||
|
another:
|
||||||
|
image: busybox:latest
|
||||||
|
command: top
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
driver: bridge
|
||||||
|
driver_opts:
|
||||||
|
"com.docker.network.bridge.enable_icc": "false"
|
||||||
4
tests/fixtures/networks/docker-compose.yml
vendored
4
tests/fixtures/networks/docker-compose.yml
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
version: 2
|
version: "2"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
|
|
@ -9,6 +9,8 @@ services:
|
||||||
image: busybox
|
image: busybox
|
||||||
command: top
|
command: top
|
||||||
networks: ["front", "back"]
|
networks: ["front", "back"]
|
||||||
|
links:
|
||||||
|
- "db:database"
|
||||||
db:
|
db:
|
||||||
image: busybox
|
image: busybox
|
||||||
command: top
|
command: top
|
||||||
|
|
|
||||||
12
tests/fixtures/networks/external-default.yml
vendored
Normal file
12
tests/fixtures/networks/external-default.yml
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
version: "2"
|
||||||
|
services:
|
||||||
|
simple:
|
||||||
|
image: busybox:latest
|
||||||
|
command: top
|
||||||
|
another:
|
||||||
|
image: busybox:latest
|
||||||
|
command: top
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
external:
|
||||||
|
name: composetest_external_network
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
version: 2
|
version: "2"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
|
|
|
||||||
2
tests/fixtures/networks/missing-network.yml
vendored
2
tests/fixtures/networks/missing-network.yml
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
version: 2
|
version: "2"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
|
|
|
||||||
16
tests/fixtures/networks/network-aliases.yml
vendored
Normal file
16
tests/fixtures/networks/network-aliases.yml
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
version: "2"
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: busybox
|
||||||
|
command: top
|
||||||
|
networks:
|
||||||
|
front:
|
||||||
|
aliases:
|
||||||
|
- forward_facing
|
||||||
|
- ahead
|
||||||
|
back:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
front: {}
|
||||||
|
back: {}
|
||||||
27
tests/fixtures/networks/network-mode.yml
vendored
Normal file
27
tests/fixtures/networks/network-mode.yml
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
version: "2"
|
||||||
|
|
||||||
|
services:
|
||||||
|
bridge:
|
||||||
|
image: busybox
|
||||||
|
command: top
|
||||||
|
network_mode: bridge
|
||||||
|
|
||||||
|
service:
|
||||||
|
image: busybox
|
||||||
|
command: top
|
||||||
|
network_mode: "service:bridge"
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: busybox
|
||||||
|
command: top
|
||||||
|
network_mode: "container:composetest_network_mode_container"
|
||||||
|
|
||||||
|
host:
|
||||||
|
image: busybox
|
||||||
|
command: top
|
||||||
|
network_mode: host
|
||||||
|
|
||||||
|
none:
|
||||||
|
image: busybox
|
||||||
|
command: top
|
||||||
|
network_mode: none
|
||||||
17
tests/fixtures/networks/predefined-networks.yml
vendored
17
tests/fixtures/networks/predefined-networks.yml
vendored
|
|
@ -1,17 +0,0 @@
|
||||||
version: 2
|
|
||||||
|
|
||||||
services:
|
|
||||||
bridge:
|
|
||||||
image: busybox
|
|
||||||
command: top
|
|
||||||
networks: ["bridge"]
|
|
||||||
|
|
||||||
host:
|
|
||||||
image: busybox
|
|
||||||
command: top
|
|
||||||
networks: ["host"]
|
|
||||||
|
|
||||||
none:
|
|
||||||
image: busybox
|
|
||||||
command: top
|
|
||||||
networks: []
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
version: 2
|
version: "2"
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
foo: {}
|
foo: {}
|
||||||
|
|
|
||||||
10
tests/fixtures/sleeps-composefile/docker-compose.yml
vendored
Normal file
10
tests/fixtures/sleeps-composefile/docker-compose.yml
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
|
||||||
|
version: "2"
|
||||||
|
|
||||||
|
services:
|
||||||
|
simple:
|
||||||
|
image: busybox:latest
|
||||||
|
command: sleep 200
|
||||||
|
another:
|
||||||
|
image: busybox:latest
|
||||||
|
command: sleep 200
|
||||||
13
tests/fixtures/v2-dependencies/docker-compose.yml
vendored
Normal file
13
tests/fixtures/v2-dependencies/docker-compose.yml
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
version: "2.0"
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: busybox:latest
|
||||||
|
command: top
|
||||||
|
web:
|
||||||
|
image: busybox:latest
|
||||||
|
command: top
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
console:
|
||||||
|
image: busybox:latest
|
||||||
|
command: top
|
||||||
2
tests/fixtures/v2-full/docker-compose.yml
vendored
2
tests/fixtures/v2-full/docker-compose.yml
vendored
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
version: 2
|
version: "2"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
data:
|
data:
|
||||||
|
|
|
||||||
2
tests/fixtures/v2-simple/docker-compose.yml
vendored
2
tests/fixtures/v2-simple/docker-compose.yml
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
version: 2
|
version: "2"
|
||||||
services:
|
services:
|
||||||
simple:
|
simple:
|
||||||
image: busybox:latest
|
image: busybox:latest
|
||||||
|
|
|
||||||
2
tests/fixtures/v2-simple/links-invalid.yml
vendored
2
tests/fixtures/v2-simple/links-invalid.yml
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
version: 2
|
version: "2"
|
||||||
services:
|
services:
|
||||||
simple:
|
simple:
|
||||||
image: busybox:latest
|
image: busybox:latest
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,13 @@ from __future__ import unicode_literals
|
||||||
import random
|
import random
|
||||||
|
|
||||||
import py
|
import py
|
||||||
|
import pytest
|
||||||
from docker.errors import NotFound
|
from docker.errors import NotFound
|
||||||
|
|
||||||
from .testcases import DockerClientTestCase
|
from .testcases import DockerClientTestCase
|
||||||
from compose.config import config
|
from compose.config import config
|
||||||
|
from compose.config import ConfigurationError
|
||||||
|
from compose.config.config import V2_0
|
||||||
from compose.config.types import VolumeFromSpec
|
from compose.config.types import VolumeFromSpec
|
||||||
from compose.config.types import VolumeSpec
|
from compose.config.types import VolumeSpec
|
||||||
from compose.const import LABEL_PROJECT
|
from compose.const import LABEL_PROJECT
|
||||||
|
|
@ -104,7 +107,71 @@ class ProjectTest(DockerClientTestCase):
|
||||||
db = project.get_service('db')
|
db = project.get_service('db')
|
||||||
self.assertEqual(db._get_volumes_from(), [data_container.id + ':rw'])
|
self.assertEqual(db._get_volumes_from(), [data_container.id + ':rw'])
|
||||||
|
|
||||||
def test_net_from_service(self):
|
@v2_only()
|
||||||
|
def test_network_mode_from_service(self):
|
||||||
|
project = Project.from_config(
|
||||||
|
name='composetest',
|
||||||
|
client=self.client,
|
||||||
|
config_data=build_service_dicts({
|
||||||
|
'version': V2_0,
|
||||||
|
'services': {
|
||||||
|
'net': {
|
||||||
|
'image': 'busybox:latest',
|
||||||
|
'command': ["top"]
|
||||||
|
},
|
||||||
|
'web': {
|
||||||
|
'image': 'busybox:latest',
|
||||||
|
'network_mode': 'service:net',
|
||||||
|
'command': ["top"]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
project.up()
|
||||||
|
|
||||||
|
web = project.get_service('web')
|
||||||
|
net = project.get_service('net')
|
||||||
|
self.assertEqual(web.network_mode.mode, 'container:' + net.containers()[0].id)
|
||||||
|
|
||||||
|
@v2_only()
|
||||||
|
def test_network_mode_from_container(self):
|
||||||
|
def get_project():
|
||||||
|
return Project.from_config(
|
||||||
|
name='composetest',
|
||||||
|
config_data=build_service_dicts({
|
||||||
|
'version': V2_0,
|
||||||
|
'services': {
|
||||||
|
'web': {
|
||||||
|
'image': 'busybox:latest',
|
||||||
|
'network_mode': 'container:composetest_net_container'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
client=self.client,
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ConfigurationError) as excinfo:
|
||||||
|
get_project()
|
||||||
|
|
||||||
|
assert "container 'composetest_net_container' which does not exist" in excinfo.exconly()
|
||||||
|
|
||||||
|
net_container = Container.create(
|
||||||
|
self.client,
|
||||||
|
image='busybox:latest',
|
||||||
|
name='composetest_net_container',
|
||||||
|
command='top',
|
||||||
|
labels={LABEL_PROJECT: 'composetest'},
|
||||||
|
)
|
||||||
|
net_container.start()
|
||||||
|
|
||||||
|
project = get_project()
|
||||||
|
project.up()
|
||||||
|
|
||||||
|
web = project.get_service('web')
|
||||||
|
self.assertEqual(web.network_mode.mode, 'container:' + net_container.id)
|
||||||
|
|
||||||
|
def test_net_from_service_v1(self):
|
||||||
project = Project.from_config(
|
project = Project.from_config(
|
||||||
name='composetest',
|
name='composetest',
|
||||||
config_data=build_service_dicts({
|
config_data=build_service_dicts({
|
||||||
|
|
@ -125,9 +192,26 @@ 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.net.mode, 'container:' + net.containers()[0].id)
|
self.assertEqual(web.network_mode.mode, 'container:' + net.containers()[0].id)
|
||||||
|
|
||||||
|
def test_net_from_container_v1(self):
|
||||||
|
def get_project():
|
||||||
|
return Project.from_config(
|
||||||
|
name='composetest',
|
||||||
|
config_data=build_service_dicts({
|
||||||
|
'web': {
|
||||||
|
'image': 'busybox:latest',
|
||||||
|
'net': 'container:composetest_net_container'
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
client=self.client,
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ConfigurationError) as excinfo:
|
||||||
|
get_project()
|
||||||
|
|
||||||
|
assert "container 'composetest_net_container' which does not exist" in excinfo.exconly()
|
||||||
|
|
||||||
def test_net_from_container(self):
|
|
||||||
net_container = Container.create(
|
net_container = Container.create(
|
||||||
self.client,
|
self.client,
|
||||||
image='busybox:latest',
|
image='busybox:latest',
|
||||||
|
|
@ -137,21 +221,11 @@ class ProjectTest(DockerClientTestCase):
|
||||||
)
|
)
|
||||||
net_container.start()
|
net_container.start()
|
||||||
|
|
||||||
project = Project.from_config(
|
project = get_project()
|
||||||
name='composetest',
|
|
||||||
config_data=build_service_dicts({
|
|
||||||
'web': {
|
|
||||||
'image': 'busybox:latest',
|
|
||||||
'net': 'container:composetest_net_container'
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
client=self.client,
|
|
||||||
)
|
|
||||||
|
|
||||||
project.up()
|
project.up()
|
||||||
|
|
||||||
web = project.get_service('web')
|
web = project.get_service('web')
|
||||||
self.assertEqual(web.net.mode, 'container:' + net_container.id)
|
self.assertEqual(web.network_mode.mode, 'container:' + net_container.id)
|
||||||
|
|
||||||
def test_start_pause_unpause_stop_kill_remove(self):
|
def test_start_pause_unpause_stop_kill_remove(self):
|
||||||
web = self.create_service('web')
|
web = self.create_service('web')
|
||||||
|
|
@ -486,11 +560,12 @@ class ProjectTest(DockerClientTestCase):
|
||||||
@v2_only()
|
@v2_only()
|
||||||
def test_project_up_networks(self):
|
def test_project_up_networks(self):
|
||||||
config_data = config.Config(
|
config_data = config.Config(
|
||||||
version=2,
|
version=V2_0,
|
||||||
services=[{
|
services=[{
|
||||||
'name': 'web',
|
'name': 'web',
|
||||||
'image': 'busybox:latest',
|
'image': 'busybox:latest',
|
||||||
'command': 'top',
|
'command': 'top',
|
||||||
|
'networks': {'foo': None, 'bar': None, 'baz': None},
|
||||||
}],
|
}],
|
||||||
volumes={},
|
volumes={},
|
||||||
networks={
|
networks={
|
||||||
|
|
@ -516,12 +591,73 @@ class ProjectTest(DockerClientTestCase):
|
||||||
foo_data = self.client.inspect_network('composetest_foo')
|
foo_data = self.client.inspect_network('composetest_foo')
|
||||||
self.assertEqual(foo_data['Driver'], 'bridge')
|
self.assertEqual(foo_data['Driver'], 'bridge')
|
||||||
|
|
||||||
|
@v2_only()
|
||||||
|
def test_up_with_ipam_config(self):
|
||||||
|
config_data = config.Config(
|
||||||
|
version=V2_0,
|
||||||
|
services=[{
|
||||||
|
'name': 'web',
|
||||||
|
'image': 'busybox:latest',
|
||||||
|
'networks': {'front': None},
|
||||||
|
}],
|
||||||
|
volumes={},
|
||||||
|
networks={
|
||||||
|
'front': {
|
||||||
|
'driver': 'bridge',
|
||||||
|
'driver_opts': {
|
||||||
|
"com.docker.network.bridge.enable_icc": "false",
|
||||||
|
},
|
||||||
|
'ipam': {
|
||||||
|
'driver': 'default',
|
||||||
|
'config': [{
|
||||||
|
"subnet": "172.28.0.0/16",
|
||||||
|
"ip_range": "172.28.5.0/24",
|
||||||
|
"gateway": "172.28.5.254",
|
||||||
|
"aux_addresses": {
|
||||||
|
"a": "172.28.1.5",
|
||||||
|
"b": "172.28.1.6",
|
||||||
|
"c": "172.28.1.7",
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
project = Project.from_config(
|
||||||
|
client=self.client,
|
||||||
|
name='composetest',
|
||||||
|
config_data=config_data,
|
||||||
|
)
|
||||||
|
project.up()
|
||||||
|
|
||||||
|
network = self.client.networks(names=['composetest_front'])[0]
|
||||||
|
|
||||||
|
assert network['Options'] == {
|
||||||
|
"com.docker.network.bridge.enable_icc": "false"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert network['IPAM'] == {
|
||||||
|
'Driver': 'default',
|
||||||
|
'Options': None,
|
||||||
|
'Config': [{
|
||||||
|
'Subnet': "172.28.0.0/16",
|
||||||
|
'IPRange': "172.28.5.0/24",
|
||||||
|
'Gateway': "172.28.5.254",
|
||||||
|
'AuxiliaryAddresses': {
|
||||||
|
'a': '172.28.1.5',
|
||||||
|
'b': '172.28.1.6',
|
||||||
|
'c': '172.28.1.7',
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
|
||||||
@v2_only()
|
@v2_only()
|
||||||
def test_project_up_volumes(self):
|
def test_project_up_volumes(self):
|
||||||
vol_name = '{0:x}'.format(random.getrandbits(32))
|
vol_name = '{0:x}'.format(random.getrandbits(32))
|
||||||
full_vol_name = 'composetest_{0}'.format(vol_name)
|
full_vol_name = 'composetest_{0}'.format(vol_name)
|
||||||
config_data = config.Config(
|
config_data = config.Config(
|
||||||
version=2,
|
version=V2_0,
|
||||||
services=[{
|
services=[{
|
||||||
'name': 'web',
|
'name': 'web',
|
||||||
'image': 'busybox:latest',
|
'image': 'busybox:latest',
|
||||||
|
|
@ -547,7 +683,7 @@ class ProjectTest(DockerClientTestCase):
|
||||||
base_file = config.ConfigFile(
|
base_file = config.ConfigFile(
|
||||||
'base.yml',
|
'base.yml',
|
||||||
{
|
{
|
||||||
'version': 2,
|
'version': V2_0,
|
||||||
'services': {
|
'services': {
|
||||||
'simple': {'image': 'busybox:latest', 'command': 'top'},
|
'simple': {'image': 'busybox:latest', 'command': 'top'},
|
||||||
'another': {
|
'another': {
|
||||||
|
|
@ -566,7 +702,7 @@ class ProjectTest(DockerClientTestCase):
|
||||||
override_file = config.ConfigFile(
|
override_file = config.ConfigFile(
|
||||||
'override.yml',
|
'override.yml',
|
||||||
{
|
{
|
||||||
'version': 2,
|
'version': V2_0,
|
||||||
'services': {
|
'services': {
|
||||||
'another': {
|
'another': {
|
||||||
'logging': {
|
'logging': {
|
||||||
|
|
@ -599,7 +735,7 @@ class ProjectTest(DockerClientTestCase):
|
||||||
vol_name = '{0:x}'.format(random.getrandbits(32))
|
vol_name = '{0:x}'.format(random.getrandbits(32))
|
||||||
full_vol_name = 'composetest_{0}'.format(vol_name)
|
full_vol_name = 'composetest_{0}'.format(vol_name)
|
||||||
config_data = config.Config(
|
config_data = config.Config(
|
||||||
version=2,
|
version=V2_0,
|
||||||
services=[{
|
services=[{
|
||||||
'name': 'web',
|
'name': 'web',
|
||||||
'image': 'busybox:latest',
|
'image': 'busybox:latest',
|
||||||
|
|
@ -613,7 +749,7 @@ class ProjectTest(DockerClientTestCase):
|
||||||
name='composetest',
|
name='composetest',
|
||||||
config_data=config_data, client=self.client
|
config_data=config_data, client=self.client
|
||||||
)
|
)
|
||||||
project.initialize_volumes()
|
project.volumes.initialize()
|
||||||
|
|
||||||
volume_data = self.client.inspect_volume(full_vol_name)
|
volume_data = self.client.inspect_volume(full_vol_name)
|
||||||
self.assertEqual(volume_data['Name'], full_vol_name)
|
self.assertEqual(volume_data['Name'], full_vol_name)
|
||||||
|
|
@ -624,7 +760,7 @@ class ProjectTest(DockerClientTestCase):
|
||||||
vol_name = '{0:x}'.format(random.getrandbits(32))
|
vol_name = '{0:x}'.format(random.getrandbits(32))
|
||||||
full_vol_name = 'composetest_{0}'.format(vol_name)
|
full_vol_name = 'composetest_{0}'.format(vol_name)
|
||||||
config_data = config.Config(
|
config_data = config.Config(
|
||||||
version=2,
|
version=V2_0,
|
||||||
services=[{
|
services=[{
|
||||||
'name': 'web',
|
'name': 'web',
|
||||||
'image': 'busybox:latest',
|
'image': 'busybox:latest',
|
||||||
|
|
@ -649,7 +785,7 @@ class ProjectTest(DockerClientTestCase):
|
||||||
vol_name = '{0:x}'.format(random.getrandbits(32))
|
vol_name = '{0:x}'.format(random.getrandbits(32))
|
||||||
|
|
||||||
config_data = config.Config(
|
config_data = config.Config(
|
||||||
version=2,
|
version=V2_0,
|
||||||
services=[{
|
services=[{
|
||||||
'name': 'web',
|
'name': 'web',
|
||||||
'image': 'busybox:latest',
|
'image': 'busybox:latest',
|
||||||
|
|
@ -664,7 +800,7 @@ class ProjectTest(DockerClientTestCase):
|
||||||
config_data=config_data, client=self.client
|
config_data=config_data, client=self.client
|
||||||
)
|
)
|
||||||
with self.assertRaises(config.ConfigurationError):
|
with self.assertRaises(config.ConfigurationError):
|
||||||
project.initialize_volumes()
|
project.volumes.initialize()
|
||||||
|
|
||||||
@v2_only()
|
@v2_only()
|
||||||
def test_initialize_volumes_updated_driver(self):
|
def test_initialize_volumes_updated_driver(self):
|
||||||
|
|
@ -672,7 +808,7 @@ class ProjectTest(DockerClientTestCase):
|
||||||
full_vol_name = 'composetest_{0}'.format(vol_name)
|
full_vol_name = 'composetest_{0}'.format(vol_name)
|
||||||
|
|
||||||
config_data = config.Config(
|
config_data = config.Config(
|
||||||
version=2,
|
version=V2_0,
|
||||||
services=[{
|
services=[{
|
||||||
'name': 'web',
|
'name': 'web',
|
||||||
'image': 'busybox:latest',
|
'image': 'busybox:latest',
|
||||||
|
|
@ -685,7 +821,7 @@ class ProjectTest(DockerClientTestCase):
|
||||||
name='composetest',
|
name='composetest',
|
||||||
config_data=config_data, client=self.client
|
config_data=config_data, client=self.client
|
||||||
)
|
)
|
||||||
project.initialize_volumes()
|
project.volumes.initialize()
|
||||||
|
|
||||||
volume_data = self.client.inspect_volume(full_vol_name)
|
volume_data = self.client.inspect_volume(full_vol_name)
|
||||||
self.assertEqual(volume_data['Name'], full_vol_name)
|
self.assertEqual(volume_data['Name'], full_vol_name)
|
||||||
|
|
@ -696,10 +832,11 @@ class ProjectTest(DockerClientTestCase):
|
||||||
)
|
)
|
||||||
project = Project.from_config(
|
project = Project.from_config(
|
||||||
name='composetest',
|
name='composetest',
|
||||||
config_data=config_data, client=self.client
|
config_data=config_data,
|
||||||
|
client=self.client
|
||||||
)
|
)
|
||||||
with self.assertRaises(config.ConfigurationError) as e:
|
with self.assertRaises(config.ConfigurationError) as e:
|
||||||
project.initialize_volumes()
|
project.volumes.initialize()
|
||||||
assert 'Configuration for volume {0} specifies driver smb'.format(
|
assert 'Configuration for volume {0} specifies driver smb'.format(
|
||||||
vol_name
|
vol_name
|
||||||
) in str(e.exception)
|
) in str(e.exception)
|
||||||
|
|
@ -711,7 +848,7 @@ class ProjectTest(DockerClientTestCase):
|
||||||
full_vol_name = 'composetest_{0}'.format(vol_name)
|
full_vol_name = 'composetest_{0}'.format(vol_name)
|
||||||
self.client.create_volume(vol_name)
|
self.client.create_volume(vol_name)
|
||||||
config_data = config.Config(
|
config_data = config.Config(
|
||||||
version=2,
|
version=V2_0,
|
||||||
services=[{
|
services=[{
|
||||||
'name': 'web',
|
'name': 'web',
|
||||||
'image': 'busybox:latest',
|
'image': 'busybox:latest',
|
||||||
|
|
@ -726,7 +863,7 @@ class ProjectTest(DockerClientTestCase):
|
||||||
name='composetest',
|
name='composetest',
|
||||||
config_data=config_data, client=self.client
|
config_data=config_data, client=self.client
|
||||||
)
|
)
|
||||||
project.initialize_volumes()
|
project.volumes.initialize()
|
||||||
|
|
||||||
with self.assertRaises(NotFound):
|
with self.assertRaises(NotFound):
|
||||||
self.client.inspect_volume(full_vol_name)
|
self.client.inspect_volume(full_vol_name)
|
||||||
|
|
@ -736,7 +873,7 @@ class ProjectTest(DockerClientTestCase):
|
||||||
vol_name = '{0:x}'.format(random.getrandbits(32))
|
vol_name = '{0:x}'.format(random.getrandbits(32))
|
||||||
|
|
||||||
config_data = config.Config(
|
config_data = config.Config(
|
||||||
version=2,
|
version=V2_0,
|
||||||
services=[{
|
services=[{
|
||||||
'name': 'web',
|
'name': 'web',
|
||||||
'image': 'busybox:latest',
|
'image': 'busybox:latest',
|
||||||
|
|
@ -752,7 +889,44 @@ class ProjectTest(DockerClientTestCase):
|
||||||
config_data=config_data, client=self.client
|
config_data=config_data, client=self.client
|
||||||
)
|
)
|
||||||
with self.assertRaises(config.ConfigurationError) as e:
|
with self.assertRaises(config.ConfigurationError) as e:
|
||||||
project.initialize_volumes()
|
project.volumes.initialize()
|
||||||
assert 'Volume {0} declared as external'.format(
|
assert 'Volume {0} declared as external'.format(
|
||||||
vol_name
|
vol_name
|
||||||
) in str(e.exception)
|
) in str(e.exception)
|
||||||
|
|
||||||
|
@v2_only()
|
||||||
|
def test_project_up_named_volumes_in_binds(self):
|
||||||
|
vol_name = '{0:x}'.format(random.getrandbits(32))
|
||||||
|
full_vol_name = 'composetest_{0}'.format(vol_name)
|
||||||
|
|
||||||
|
base_file = config.ConfigFile(
|
||||||
|
'base.yml',
|
||||||
|
{
|
||||||
|
'version': V2_0,
|
||||||
|
'services': {
|
||||||
|
'simple': {
|
||||||
|
'image': 'busybox:latest',
|
||||||
|
'command': 'top',
|
||||||
|
'volumes': ['{0}:/data'.format(vol_name)]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'volumes': {
|
||||||
|
vol_name: {'driver': 'local'}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
config_details = config.ConfigDetails('.', [base_file])
|
||||||
|
config_data = config.load(config_details)
|
||||||
|
project = Project.from_config(
|
||||||
|
name='composetest', config_data=config_data, client=self.client
|
||||||
|
)
|
||||||
|
service = project.services[0]
|
||||||
|
self.assertEqual(service.name, 'simple')
|
||||||
|
volumes = service.options.get('volumes')
|
||||||
|
self.assertEqual(len(volumes), 1)
|
||||||
|
self.assertEqual(volumes[0].external, full_vol_name)
|
||||||
|
project.up()
|
||||||
|
engine_volumes = self.client.volumes()['Volumes']
|
||||||
|
container = service.get_container()
|
||||||
|
assert [mount['Name'] for mount in container.get('Mounts')] == [full_vol_name]
|
||||||
|
assert next((v for v in engine_volumes if v['Name'] == vol_name), None) is None
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import tempfile
|
||||||
from os import path
|
from os import path
|
||||||
|
|
||||||
from docker.errors import APIError
|
from docker.errors import APIError
|
||||||
from pytest import mark
|
|
||||||
from six import StringIO
|
from six import StringIO
|
||||||
from six import text_type
|
from six import text_type
|
||||||
|
|
||||||
|
|
@ -27,7 +26,7 @@ from compose.const import LABEL_VERSION
|
||||||
from compose.container import Container
|
from compose.container import Container
|
||||||
from compose.service import ConvergencePlan
|
from compose.service import ConvergencePlan
|
||||||
from compose.service import ConvergenceStrategy
|
from compose.service import ConvergenceStrategy
|
||||||
from compose.service import Net
|
from compose.service import NetworkMode
|
||||||
from compose.service import Service
|
from compose.service import Service
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -267,6 +266,30 @@ class ServiceTest(DockerClientTestCase):
|
||||||
self.client.inspect_container,
|
self.client.inspect_container,
|
||||||
old_container.id)
|
old_container.id)
|
||||||
|
|
||||||
|
def test_execute_convergence_plan_recreate_twice(self):
|
||||||
|
service = self.create_service(
|
||||||
|
'db',
|
||||||
|
volumes=[VolumeSpec.parse('/etc')],
|
||||||
|
entrypoint=['top'],
|
||||||
|
command=['-d', '1'])
|
||||||
|
|
||||||
|
orig_container = service.create_container()
|
||||||
|
service.start_container(orig_container)
|
||||||
|
|
||||||
|
orig_container.inspect() # reload volume data
|
||||||
|
volume_path = orig_container.get_mount('/etc')['Source']
|
||||||
|
|
||||||
|
# Do this twice to reproduce the bug
|
||||||
|
for _ in range(2):
|
||||||
|
new_container, = service.execute_convergence_plan(
|
||||||
|
ConvergencePlan('recreate', [orig_container]))
|
||||||
|
|
||||||
|
assert new_container.get_mount('/etc')['Source'] == volume_path
|
||||||
|
assert ('affinity:container==%s' % orig_container.id in
|
||||||
|
new_container.get('Config.Env'))
|
||||||
|
|
||||||
|
orig_container = new_container
|
||||||
|
|
||||||
def test_execute_convergence_plan_when_containers_are_stopped(self):
|
def test_execute_convergence_plan_when_containers_are_stopped(self):
|
||||||
service = self.create_service(
|
service = self.create_service(
|
||||||
'db',
|
'db',
|
||||||
|
|
@ -344,6 +367,31 @@ class ServiceTest(DockerClientTestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(new_container.get_mount('/data')['Source'], volume_path)
|
self.assertEqual(new_container.get_mount('/data')['Source'], volume_path)
|
||||||
|
|
||||||
|
def test_execute_convergence_plan_when_host_volume_is_removed(self):
|
||||||
|
host_path = '/tmp/host-path'
|
||||||
|
service = self.create_service(
|
||||||
|
'db',
|
||||||
|
build={'context': 'tests/fixtures/dockerfile-with-volume'},
|
||||||
|
volumes=[VolumeSpec(host_path, '/data', 'rw')])
|
||||||
|
|
||||||
|
old_container = create_and_start_container(service)
|
||||||
|
assert (
|
||||||
|
[mount['Destination'] for mount in old_container.get('Mounts')] ==
|
||||||
|
['/data']
|
||||||
|
)
|
||||||
|
service.options['volumes'] = []
|
||||||
|
|
||||||
|
with mock.patch('compose.service.log', autospec=True) as mock_log:
|
||||||
|
new_container, = service.execute_convergence_plan(
|
||||||
|
ConvergencePlan('recreate', [old_container]))
|
||||||
|
|
||||||
|
assert not mock_log.warn.called
|
||||||
|
assert (
|
||||||
|
[mount['Destination'] for mount in new_container.get('Mounts')],
|
||||||
|
['/data']
|
||||||
|
)
|
||||||
|
assert new_container.get_mount('/data')['Source'] != host_path
|
||||||
|
|
||||||
def test_execute_convergence_plan_without_start(self):
|
def test_execute_convergence_plan_without_start(self):
|
||||||
service = self.create_service(
|
service = self.create_service(
|
||||||
'db',
|
'db',
|
||||||
|
|
@ -372,7 +420,6 @@ class ServiceTest(DockerClientTestCase):
|
||||||
create_and_start_container(db)
|
create_and_start_container(db)
|
||||||
self.assertEqual(db.containers()[0].environment['FOO'], 'BAR')
|
self.assertEqual(db.containers()[0].environment['FOO'], 'BAR')
|
||||||
|
|
||||||
@mark.skipif(True, reason="Engine returns error - needs investigating")
|
|
||||||
def test_start_container_creates_links(self):
|
def test_start_container_creates_links(self):
|
||||||
db = self.create_service('db')
|
db = self.create_service('db')
|
||||||
web = self.create_service('web', links=[(db, None)])
|
web = self.create_service('web', links=[(db, None)])
|
||||||
|
|
@ -389,7 +436,6 @@ class ServiceTest(DockerClientTestCase):
|
||||||
'db'])
|
'db'])
|
||||||
)
|
)
|
||||||
|
|
||||||
@mark.skipif(True, reason="Engine returns error - needs investigating")
|
|
||||||
def test_start_container_creates_links_with_names(self):
|
def test_start_container_creates_links_with_names(self):
|
||||||
db = self.create_service('db')
|
db = self.create_service('db')
|
||||||
web = self.create_service('web', links=[(db, 'custom_link_name')])
|
web = self.create_service('web', links=[(db, 'custom_link_name')])
|
||||||
|
|
@ -433,7 +479,6 @@ class ServiceTest(DockerClientTestCase):
|
||||||
c = create_and_start_container(db)
|
c = create_and_start_container(db)
|
||||||
self.assertEqual(set(get_links(c)), set([]))
|
self.assertEqual(set(get_links(c)), set([]))
|
||||||
|
|
||||||
@mark.skipif(True, reason="Engine returns error - needs investigating")
|
|
||||||
def test_start_one_off_container_creates_links_to_its_own_service(self):
|
def test_start_one_off_container_creates_links_to_its_own_service(self):
|
||||||
db = self.create_service('db')
|
db = self.create_service('db')
|
||||||
|
|
||||||
|
|
@ -750,18 +795,23 @@ class ServiceTest(DockerClientTestCase):
|
||||||
for container in containers:
|
for container in containers:
|
||||||
self.assertEqual(list(container.inspect()['HostConfig']['PortBindings'].keys()), ['8000/tcp'])
|
self.assertEqual(list(container.inspect()['HostConfig']['PortBindings'].keys()), ['8000/tcp'])
|
||||||
|
|
||||||
|
def test_scale_with_immediate_exit(self):
|
||||||
|
service = self.create_service('web', image='busybox', command='true')
|
||||||
|
service.scale(2)
|
||||||
|
assert len(service.containers(stopped=True)) == 2
|
||||||
|
|
||||||
def test_network_mode_none(self):
|
def test_network_mode_none(self):
|
||||||
service = self.create_service('web', net=Net('none'))
|
service = self.create_service('web', network_mode=NetworkMode('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=Net('bridge'))
|
service = self.create_service('web', network_mode=NetworkMode('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=Net('host'))
|
service = self.create_service('web', network_mode=NetworkMode('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')
|
||||||
|
|
||||||
|
|
@ -859,7 +909,7 @@ class ServiceTest(DockerClientTestCase):
|
||||||
'FILE_DEF': 'F1',
|
'FILE_DEF': 'F1',
|
||||||
'FILE_DEF_EMPTY': '',
|
'FILE_DEF_EMPTY': '',
|
||||||
'ENV_DEF': 'E3',
|
'ENV_DEF': 'E3',
|
||||||
'NO_DEF': ''
|
'NO_DEF': None
|
||||||
}.items():
|
}.items():
|
||||||
self.assertEqual(env[k], v)
|
self.assertEqual(env[k], v)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ from pytest import skip
|
||||||
from .. import unittest
|
from .. import unittest
|
||||||
from compose.cli.docker_client import docker_client
|
from compose.cli.docker_client import docker_client
|
||||||
from compose.config.config import resolve_environment
|
from compose.config.config import resolve_environment
|
||||||
|
from compose.config.config import V1
|
||||||
|
from compose.config.config import V2_0
|
||||||
from compose.const import API_VERSIONS
|
from compose.const import API_VERSIONS
|
||||||
from compose.const import LABEL_PROJECT
|
from compose.const import LABEL_PROJECT
|
||||||
from compose.progress_stream import stream_output
|
from compose.progress_stream import stream_output
|
||||||
|
|
@ -54,9 +56,9 @@ class DockerClientTestCase(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
if engine_version_too_low_for_v2():
|
if engine_version_too_low_for_v2():
|
||||||
version = API_VERSIONS[1]
|
version = API_VERSIONS[V1]
|
||||||
else:
|
else:
|
||||||
version = API_VERSIONS[2]
|
version = API_VERSIONS[V2_0]
|
||||||
|
|
||||||
cls.client = docker_client(version)
|
cls.client = docker_client(version)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,9 @@ import logging
|
||||||
from compose import container
|
from compose import container
|
||||||
from compose.cli.errors import UserError
|
from compose.cli.errors import UserError
|
||||||
from compose.cli.formatter import ConsoleWarningFormatter
|
from compose.cli.formatter import ConsoleWarningFormatter
|
||||||
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.cli.main import build_log_printer
|
||||||
from compose.cli.main import convergence_strategy_from_opts
|
from compose.cli.main import convergence_strategy_from_opts
|
||||||
from compose.cli.main import setup_console_handler
|
from compose.cli.main import setup_console_handler
|
||||||
from compose.project import Project
|
|
||||||
from compose.service import ConvergenceStrategy
|
from compose.service import ConvergenceStrategy
|
||||||
from tests import mock
|
from tests import mock
|
||||||
from tests import unittest
|
from tests import unittest
|
||||||
|
|
@ -49,21 +46,6 @@ class CLIMainTestCase(unittest.TestCase):
|
||||||
log_printer = build_log_printer(containers, service_names, True, False)
|
log_printer = build_log_printer(containers, service_names, True, False)
|
||||||
self.assertEqual(log_printer.containers, containers)
|
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.signals.signal', autospec=True) as mock_signal:
|
|
||||||
attach_to_logs(project, log_printer, service_names, timeout)
|
|
||||||
|
|
||||||
assert mock_signal.signal.mock_calls == [
|
|
||||||
mock.call(mock_signal.SIGINT, mock.ANY),
|
|
||||||
mock.call(mock_signal.SIGTERM, mock.ANY),
|
|
||||||
]
|
|
||||||
log_printer.run.assert_called_once_with()
|
|
||||||
|
|
||||||
|
|
||||||
class SetupConsoleHandlerTestCase(unittest.TestCase):
|
class SetupConsoleHandlerTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,13 @@ class CLITestCase(unittest.TestCase):
|
||||||
project_name = get_project_name(None)
|
project_name = get_project_name(None)
|
||||||
self.assertEquals(project_name, name)
|
self.assertEquals(project_name, name)
|
||||||
|
|
||||||
|
def test_project_name_with_empty_environment_var(self):
|
||||||
|
base_dir = 'tests/fixtures/simple-composefile'
|
||||||
|
with mock.patch.dict(os.environ):
|
||||||
|
os.environ['COMPOSE_PROJECT_NAME'] = ''
|
||||||
|
project_name = get_project_name(base_dir)
|
||||||
|
self.assertEquals('simplecomposefile', project_name)
|
||||||
|
|
||||||
def test_get_project(self):
|
def test_get_project(self):
|
||||||
base_dir = 'tests/fixtures/longer-filename-composefile'
|
base_dir = 'tests/fixtures/longer-filename-composefile'
|
||||||
project = get_project(base_dir)
|
project = get_project(base_dir)
|
||||||
|
|
@ -72,8 +79,40 @@ class CLITestCase(unittest.TestCase):
|
||||||
TopLevelCommand().dispatch(['help', 'nonexistent'], None)
|
TopLevelCommand().dispatch(['help', 'nonexistent'], None)
|
||||||
|
|
||||||
@pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason="requires dockerpty")
|
@pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason="requires dockerpty")
|
||||||
@mock.patch('compose.cli.main.dockerpty', autospec=True)
|
@mock.patch('compose.cli.main.RunOperation', autospec=True)
|
||||||
def test_run_with_environment_merged_with_options_list(self, mock_dockerpty):
|
@mock.patch('compose.cli.main.PseudoTerminal', autospec=True)
|
||||||
|
def test_run_interactive_passes_logs_false(self, mock_pseudo_terminal, mock_run_operation):
|
||||||
|
command = TopLevelCommand()
|
||||||
|
mock_client = mock.create_autospec(docker.Client)
|
||||||
|
mock_project = mock.Mock(client=mock_client)
|
||||||
|
mock_project.get_service.return_value = Service(
|
||||||
|
'service',
|
||||||
|
client=mock_client,
|
||||||
|
environment=['FOO=ONE', 'BAR=TWO'],
|
||||||
|
image='someimage')
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
command.run(mock_project, {
|
||||||
|
'SERVICE': 'service',
|
||||||
|
'COMMAND': None,
|
||||||
|
'-e': ['BAR=NEW', 'OTHER=bär'.encode('utf-8')],
|
||||||
|
'--user': None,
|
||||||
|
'--no-deps': None,
|
||||||
|
'-d': False,
|
||||||
|
'-T': None,
|
||||||
|
'--entrypoint': None,
|
||||||
|
'--service-ports': None,
|
||||||
|
'--publish': [],
|
||||||
|
'--rm': None,
|
||||||
|
'--name': None,
|
||||||
|
})
|
||||||
|
|
||||||
|
_, _, call_kwargs = mock_run_operation.mock_calls[0]
|
||||||
|
assert call_kwargs['logs'] is False
|
||||||
|
|
||||||
|
@pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason="requires dockerpty")
|
||||||
|
@mock.patch('compose.cli.main.PseudoTerminal', autospec=True)
|
||||||
|
def test_run_with_environment_merged_with_options_list(self, mock_pseudo_terminal):
|
||||||
command = TopLevelCommand()
|
command = TopLevelCommand()
|
||||||
mock_client = mock.create_autospec(docker.Client)
|
mock_client = mock.create_autospec(docker.Client)
|
||||||
mock_project = mock.Mock(client=mock_client)
|
mock_project = mock.Mock(client=mock_client)
|
||||||
|
|
@ -99,9 +138,10 @@ class CLITestCase(unittest.TestCase):
|
||||||
})
|
})
|
||||||
|
|
||||||
_, _, call_kwargs = mock_client.create_container.mock_calls[0]
|
_, _, call_kwargs = mock_client.create_container.mock_calls[0]
|
||||||
self.assertEqual(
|
assert (
|
||||||
call_kwargs['environment'],
|
sorted(call_kwargs['environment']) ==
|
||||||
{'FOO': 'ONE', 'BAR': 'NEW', 'OTHER': u'bär'})
|
sorted(['FOO=ONE', 'BAR=NEW', 'OTHER=bär'])
|
||||||
|
)
|
||||||
|
|
||||||
def test_run_service_with_restart_always(self):
|
def test_run_service_with_restart_always(self):
|
||||||
command = TopLevelCommand()
|
command = TopLevelCommand()
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,13 +1,14 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from compose.config.errors import DependencyError
|
from compose.config.errors import DependencyError
|
||||||
from compose.config.sort_services import sort_service_dicts
|
from compose.config.sort_services import sort_service_dicts
|
||||||
from compose.config.types import VolumeFromSpec
|
from compose.config.types import VolumeFromSpec
|
||||||
from tests import unittest
|
|
||||||
|
|
||||||
|
|
||||||
class SortServiceTest(unittest.TestCase):
|
class TestSortService(object):
|
||||||
def test_sort_service_dicts_1(self):
|
def test_sort_service_dicts_1(self):
|
||||||
services = [
|
services = [
|
||||||
{
|
{
|
||||||
|
|
@ -23,10 +24,10 @@ class SortServiceTest(unittest.TestCase):
|
||||||
]
|
]
|
||||||
|
|
||||||
sorted_services = sort_service_dicts(services)
|
sorted_services = sort_service_dicts(services)
|
||||||
self.assertEqual(len(sorted_services), 3)
|
assert len(sorted_services) == 3
|
||||||
self.assertEqual(sorted_services[0]['name'], 'grunt')
|
assert sorted_services[0]['name'] == 'grunt'
|
||||||
self.assertEqual(sorted_services[1]['name'], 'redis')
|
assert sorted_services[1]['name'] == 'redis'
|
||||||
self.assertEqual(sorted_services[2]['name'], 'web')
|
assert sorted_services[2]['name'] == 'web'
|
||||||
|
|
||||||
def test_sort_service_dicts_2(self):
|
def test_sort_service_dicts_2(self):
|
||||||
services = [
|
services = [
|
||||||
|
|
@ -44,10 +45,10 @@ class SortServiceTest(unittest.TestCase):
|
||||||
]
|
]
|
||||||
|
|
||||||
sorted_services = sort_service_dicts(services)
|
sorted_services = sort_service_dicts(services)
|
||||||
self.assertEqual(len(sorted_services), 3)
|
assert len(sorted_services) == 3
|
||||||
self.assertEqual(sorted_services[0]['name'], 'redis')
|
assert sorted_services[0]['name'] == 'redis'
|
||||||
self.assertEqual(sorted_services[1]['name'], 'postgres')
|
assert sorted_services[1]['name'] == 'postgres'
|
||||||
self.assertEqual(sorted_services[2]['name'], 'web')
|
assert sorted_services[2]['name'] == 'web'
|
||||||
|
|
||||||
def test_sort_service_dicts_3(self):
|
def test_sort_service_dicts_3(self):
|
||||||
services = [
|
services = [
|
||||||
|
|
@ -65,10 +66,10 @@ class SortServiceTest(unittest.TestCase):
|
||||||
]
|
]
|
||||||
|
|
||||||
sorted_services = sort_service_dicts(services)
|
sorted_services = sort_service_dicts(services)
|
||||||
self.assertEqual(len(sorted_services), 3)
|
assert len(sorted_services) == 3
|
||||||
self.assertEqual(sorted_services[0]['name'], 'child')
|
assert sorted_services[0]['name'] == 'child'
|
||||||
self.assertEqual(sorted_services[1]['name'], 'parent')
|
assert sorted_services[1]['name'] == 'parent'
|
||||||
self.assertEqual(sorted_services[2]['name'], 'grandparent')
|
assert sorted_services[2]['name'] == 'grandparent'
|
||||||
|
|
||||||
def test_sort_service_dicts_4(self):
|
def test_sort_service_dicts_4(self):
|
||||||
services = [
|
services = [
|
||||||
|
|
@ -86,10 +87,10 @@ class SortServiceTest(unittest.TestCase):
|
||||||
]
|
]
|
||||||
|
|
||||||
sorted_services = sort_service_dicts(services)
|
sorted_services = sort_service_dicts(services)
|
||||||
self.assertEqual(len(sorted_services), 3)
|
assert len(sorted_services) == 3
|
||||||
self.assertEqual(sorted_services[0]['name'], 'child')
|
assert sorted_services[0]['name'] == 'child'
|
||||||
self.assertEqual(sorted_services[1]['name'], 'parent')
|
assert sorted_services[1]['name'] == 'parent'
|
||||||
self.assertEqual(sorted_services[2]['name'], 'grandparent')
|
assert sorted_services[2]['name'] == 'grandparent'
|
||||||
|
|
||||||
def test_sort_service_dicts_5(self):
|
def test_sort_service_dicts_5(self):
|
||||||
services = [
|
services = [
|
||||||
|
|
@ -99,7 +100,7 @@ class SortServiceTest(unittest.TestCase):
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'parent',
|
'name': 'parent',
|
||||||
'net': 'container:child'
|
'network_mode': 'service:child'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'child'
|
'name': 'child'
|
||||||
|
|
@ -107,10 +108,10 @@ class SortServiceTest(unittest.TestCase):
|
||||||
]
|
]
|
||||||
|
|
||||||
sorted_services = sort_service_dicts(services)
|
sorted_services = sort_service_dicts(services)
|
||||||
self.assertEqual(len(sorted_services), 3)
|
assert len(sorted_services) == 3
|
||||||
self.assertEqual(sorted_services[0]['name'], 'child')
|
assert sorted_services[0]['name'] == 'child'
|
||||||
self.assertEqual(sorted_services[1]['name'], 'parent')
|
assert sorted_services[1]['name'] == 'parent'
|
||||||
self.assertEqual(sorted_services[2]['name'], 'grandparent')
|
assert sorted_services[2]['name'] == 'grandparent'
|
||||||
|
|
||||||
def test_sort_service_dicts_6(self):
|
def test_sort_service_dicts_6(self):
|
||||||
services = [
|
services = [
|
||||||
|
|
@ -128,15 +129,15 @@ class SortServiceTest(unittest.TestCase):
|
||||||
]
|
]
|
||||||
|
|
||||||
sorted_services = sort_service_dicts(services)
|
sorted_services = sort_service_dicts(services)
|
||||||
self.assertEqual(len(sorted_services), 3)
|
assert len(sorted_services) == 3
|
||||||
self.assertEqual(sorted_services[0]['name'], 'child')
|
assert sorted_services[0]['name'] == 'child'
|
||||||
self.assertEqual(sorted_services[1]['name'], 'parent')
|
assert sorted_services[1]['name'] == 'parent'
|
||||||
self.assertEqual(sorted_services[2]['name'], 'grandparent')
|
assert sorted_services[2]['name'] == 'grandparent'
|
||||||
|
|
||||||
def test_sort_service_dicts_7(self):
|
def test_sort_service_dicts_7(self):
|
||||||
services = [
|
services = [
|
||||||
{
|
{
|
||||||
'net': 'container:three',
|
'network_mode': 'service:three',
|
||||||
'name': 'four'
|
'name': 'four'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -153,11 +154,11 @@ class SortServiceTest(unittest.TestCase):
|
||||||
]
|
]
|
||||||
|
|
||||||
sorted_services = sort_service_dicts(services)
|
sorted_services = sort_service_dicts(services)
|
||||||
self.assertEqual(len(sorted_services), 4)
|
assert len(sorted_services) == 4
|
||||||
self.assertEqual(sorted_services[0]['name'], 'one')
|
assert sorted_services[0]['name'] == 'one'
|
||||||
self.assertEqual(sorted_services[1]['name'], 'two')
|
assert sorted_services[1]['name'] == 'two'
|
||||||
self.assertEqual(sorted_services[2]['name'], 'three')
|
assert sorted_services[2]['name'] == 'three'
|
||||||
self.assertEqual(sorted_services[3]['name'], 'four')
|
assert sorted_services[3]['name'] == 'four'
|
||||||
|
|
||||||
def test_sort_service_dicts_circular_imports(self):
|
def test_sort_service_dicts_circular_imports(self):
|
||||||
services = [
|
services = [
|
||||||
|
|
@ -171,13 +172,10 @@ class SortServiceTest(unittest.TestCase):
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
try:
|
with pytest.raises(DependencyError) as exc:
|
||||||
sort_service_dicts(services)
|
sort_service_dicts(services)
|
||||||
except DependencyError as e:
|
assert 'redis' in exc.exconly()
|
||||||
self.assertIn('redis', e.msg)
|
assert 'web' in exc.exconly()
|
||||||
self.assertIn('web', e.msg)
|
|
||||||
else:
|
|
||||||
self.fail('Should have thrown an DependencyError')
|
|
||||||
|
|
||||||
def test_sort_service_dicts_circular_imports_2(self):
|
def test_sort_service_dicts_circular_imports_2(self):
|
||||||
services = [
|
services = [
|
||||||
|
|
@ -194,13 +192,10 @@ class SortServiceTest(unittest.TestCase):
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
try:
|
with pytest.raises(DependencyError) as exc:
|
||||||
sort_service_dicts(services)
|
sort_service_dicts(services)
|
||||||
except DependencyError as e:
|
assert 'redis' in exc.exconly()
|
||||||
self.assertIn('redis', e.msg)
|
assert 'web' in exc.exconly()
|
||||||
self.assertIn('web', e.msg)
|
|
||||||
else:
|
|
||||||
self.fail('Should have thrown an DependencyError')
|
|
||||||
|
|
||||||
def test_sort_service_dicts_circular_imports_3(self):
|
def test_sort_service_dicts_circular_imports_3(self):
|
||||||
services = [
|
services = [
|
||||||
|
|
@ -218,13 +213,10 @@ class SortServiceTest(unittest.TestCase):
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
try:
|
with pytest.raises(DependencyError) as exc:
|
||||||
sort_service_dicts(services)
|
sort_service_dicts(services)
|
||||||
except DependencyError as e:
|
assert 'a' in exc.exconly()
|
||||||
self.assertIn('a', e.msg)
|
assert 'b' in exc.exconly()
|
||||||
self.assertIn('b', e.msg)
|
|
||||||
else:
|
|
||||||
self.fail('Should have thrown an DependencyError')
|
|
||||||
|
|
||||||
def test_sort_service_dicts_self_imports(self):
|
def test_sort_service_dicts_self_imports(self):
|
||||||
services = [
|
services = [
|
||||||
|
|
@ -234,9 +226,18 @@ class SortServiceTest(unittest.TestCase):
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
try:
|
with pytest.raises(DependencyError) as exc:
|
||||||
sort_service_dicts(services)
|
sort_service_dicts(services)
|
||||||
except DependencyError as e:
|
assert 'web' in exc.exconly()
|
||||||
self.assertIn('web', e.msg)
|
|
||||||
else:
|
def test_sort_service_dicts_depends_on_self(self):
|
||||||
self.fail('Should have thrown an DependencyError')
|
services = [
|
||||||
|
{
|
||||||
|
'depends_on': ['web'],
|
||||||
|
'name': 'web'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
with pytest.raises(DependencyError) as exc:
|
||||||
|
sort_service_dicts(services)
|
||||||
|
assert 'A service can not depend on itself: web' in exc.exconly()
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,13 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from compose.config.config import V1
|
||||||
|
from compose.config.config import V2_0
|
||||||
from compose.config.errors import ConfigurationError
|
from compose.config.errors import ConfigurationError
|
||||||
from compose.config.types import parse_extra_hosts
|
from compose.config.types import parse_extra_hosts
|
||||||
from compose.config.types import VolumeFromSpec
|
from compose.config.types import VolumeFromSpec
|
||||||
from compose.config.types import VolumeSpec
|
from compose.config.types import VolumeSpec
|
||||||
from compose.const import IS_WINDOWS_PLATFORM
|
from compose.const import IS_WINDOWS_PLATFORM
|
||||||
from tests.unit.config.config_test import V1
|
|
||||||
from tests.unit.config.config_test import V2
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_extra_hosts_list():
|
def test_parse_extra_hosts_list():
|
||||||
|
|
@ -91,26 +91,26 @@ class TestVolumesFromSpec(object):
|
||||||
VolumeFromSpec.parse('unknown:format:ro', self.services, V1)
|
VolumeFromSpec.parse('unknown:format:ro', self.services, V1)
|
||||||
|
|
||||||
def test_parse_v2_from_service(self):
|
def test_parse_v2_from_service(self):
|
||||||
volume_from = VolumeFromSpec.parse('servicea', self.services, V2)
|
volume_from = VolumeFromSpec.parse('servicea', self.services, V2_0)
|
||||||
assert volume_from == VolumeFromSpec('servicea', 'rw', 'service')
|
assert volume_from == VolumeFromSpec('servicea', 'rw', 'service')
|
||||||
|
|
||||||
def test_parse_v2_from_service_with_mode(self):
|
def test_parse_v2_from_service_with_mode(self):
|
||||||
volume_from = VolumeFromSpec.parse('servicea:ro', self.services, V2)
|
volume_from = VolumeFromSpec.parse('servicea:ro', self.services, V2_0)
|
||||||
assert volume_from == VolumeFromSpec('servicea', 'ro', 'service')
|
assert volume_from == VolumeFromSpec('servicea', 'ro', 'service')
|
||||||
|
|
||||||
def test_parse_v2_from_container(self):
|
def test_parse_v2_from_container(self):
|
||||||
volume_from = VolumeFromSpec.parse('container:foo', self.services, V2)
|
volume_from = VolumeFromSpec.parse('container:foo', self.services, V2_0)
|
||||||
assert volume_from == VolumeFromSpec('foo', 'rw', 'container')
|
assert volume_from == VolumeFromSpec('foo', 'rw', 'container')
|
||||||
|
|
||||||
def test_parse_v2_from_container_with_mode(self):
|
def test_parse_v2_from_container_with_mode(self):
|
||||||
volume_from = VolumeFromSpec.parse('container:foo:ro', self.services, V2)
|
volume_from = VolumeFromSpec.parse('container:foo:ro', self.services, V2_0)
|
||||||
assert volume_from == VolumeFromSpec('foo', 'ro', 'container')
|
assert volume_from == VolumeFromSpec('foo', 'ro', 'container')
|
||||||
|
|
||||||
def test_parse_v2_invalid_type(self):
|
def test_parse_v2_invalid_type(self):
|
||||||
with pytest.raises(ConfigurationError) as exc:
|
with pytest.raises(ConfigurationError) as exc:
|
||||||
VolumeFromSpec.parse('bogus:foo:ro', self.services, V2)
|
VolumeFromSpec.parse('bogus:foo:ro', self.services, V2_0)
|
||||||
assert "Unknown volumes_from type 'bogus'" in exc.exconly()
|
assert "Unknown volumes_from type 'bogus'" in exc.exconly()
|
||||||
|
|
||||||
def test_parse_v2_invalid(self):
|
def test_parse_v2_invalid(self):
|
||||||
with pytest.raises(ConfigurationError):
|
with pytest.raises(ConfigurationError):
|
||||||
VolumeFromSpec.parse('unknown:format:ro', self.services, V2)
|
VolumeFromSpec.parse('unknown:format:ro', self.services, V2_0)
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,9 @@ from compose.container import get_container_name
|
||||||
class ContainerTest(unittest.TestCase):
|
class ContainerTest(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
self.container_id = "abcabcabcbabc12345"
|
||||||
self.container_dict = {
|
self.container_dict = {
|
||||||
"Id": "abc",
|
"Id": self.container_id,
|
||||||
"Image": "busybox:latest",
|
"Image": "busybox:latest",
|
||||||
"Command": "top",
|
"Command": "top",
|
||||||
"Created": 1387384730,
|
"Created": 1387384730,
|
||||||
|
|
@ -41,19 +42,22 @@ class ContainerTest(unittest.TestCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
container.dictionary,
|
container.dictionary,
|
||||||
{
|
{
|
||||||
"Id": "abc",
|
"Id": self.container_id,
|
||||||
"Image": "busybox:latest",
|
"Image": "busybox:latest",
|
||||||
"Name": "/composetest_db_1",
|
"Name": "/composetest_db_1",
|
||||||
})
|
})
|
||||||
|
|
||||||
def test_from_ps_prefixed(self):
|
def test_from_ps_prefixed(self):
|
||||||
self.container_dict['Names'] = ['/swarm-host-1' + n for n in self.container_dict['Names']]
|
self.container_dict['Names'] = [
|
||||||
|
'/swarm-host-1' + n for n in self.container_dict['Names']
|
||||||
|
]
|
||||||
|
|
||||||
container = Container.from_ps(None,
|
container = Container.from_ps(
|
||||||
self.container_dict,
|
None,
|
||||||
has_been_inspected=True)
|
self.container_dict,
|
||||||
|
has_been_inspected=True)
|
||||||
self.assertEqual(container.dictionary, {
|
self.assertEqual(container.dictionary, {
|
||||||
"Id": "abc",
|
"Id": self.container_id,
|
||||||
"Image": "busybox:latest",
|
"Image": "busybox:latest",
|
||||||
"Name": "/composetest_db_1",
|
"Name": "/composetest_db_1",
|
||||||
})
|
})
|
||||||
|
|
@ -142,6 +146,10 @@ class ContainerTest(unittest.TestCase):
|
||||||
self.assertEqual(container.get('HostConfig.VolumesFrom'), ["volume_id"])
|
self.assertEqual(container.get('HostConfig.VolumesFrom'), ["volume_id"])
|
||||||
self.assertEqual(container.get('Foo.Bar.DoesNotExist'), None)
|
self.assertEqual(container.get('Foo.Bar.DoesNotExist'), None)
|
||||||
|
|
||||||
|
def test_short_id(self):
|
||||||
|
container = Container(None, self.container_dict, has_been_inspected=True)
|
||||||
|
assert container.short_id == self.container_id[:12]
|
||||||
|
|
||||||
|
|
||||||
class GetContainerNameTestCase(unittest.TestCase):
|
class GetContainerNameTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ class ProjectTest(unittest.TestCase):
|
||||||
self.assertEqual(project.get_service('web').options['image'], 'busybox:latest')
|
self.assertEqual(project.get_service('web').options['image'], 'busybox:latest')
|
||||||
self.assertEqual(project.get_service('db').name, 'db')
|
self.assertEqual(project.get_service('db').name, 'db')
|
||||||
self.assertEqual(project.get_service('db').options['image'], 'busybox:latest')
|
self.assertEqual(project.get_service('db').options['image'], 'busybox:latest')
|
||||||
self.assertFalse(project.use_networking)
|
self.assertFalse(project.networks.use_networking)
|
||||||
|
|
||||||
def test_from_config_v2(self):
|
def test_from_config_v2(self):
|
||||||
config = Config(
|
config = Config(
|
||||||
|
|
@ -65,7 +65,7 @@ class ProjectTest(unittest.TestCase):
|
||||||
)
|
)
|
||||||
project = Project.from_config('composetest', config, None)
|
project = Project.from_config('composetest', config, None)
|
||||||
self.assertEqual(len(project.services), 2)
|
self.assertEqual(len(project.services), 2)
|
||||||
self.assertTrue(project.use_networking)
|
self.assertTrue(project.networks.use_networking)
|
||||||
|
|
||||||
def test_get_service(self):
|
def test_get_service(self):
|
||||||
web = Service(
|
web = Service(
|
||||||
|
|
@ -349,7 +349,7 @@ class ProjectTest(unittest.TestCase):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
service = project.get_service('test')
|
service = project.get_service('test')
|
||||||
self.assertEqual(service.net.id, None)
|
self.assertEqual(service.network_mode.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):
|
||||||
|
|
@ -365,7 +365,7 @@ class ProjectTest(unittest.TestCase):
|
||||||
{
|
{
|
||||||
'name': 'test',
|
'name': 'test',
|
||||||
'image': 'busybox:latest',
|
'image': 'busybox:latest',
|
||||||
'net': 'container:aaa'
|
'network_mode': 'container:aaa'
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
networks=None,
|
networks=None,
|
||||||
|
|
@ -373,7 +373,7 @@ class ProjectTest(unittest.TestCase):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
service = project.get_service('test')
|
service = project.get_service('test')
|
||||||
self.assertEqual(service.net.mode, 'container:' + container_id)
|
self.assertEqual(service.network_mode.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'
|
||||||
|
|
@ -398,7 +398,7 @@ class ProjectTest(unittest.TestCase):
|
||||||
{
|
{
|
||||||
'name': 'test',
|
'name': 'test',
|
||||||
'image': 'busybox:latest',
|
'image': 'busybox:latest',
|
||||||
'net': 'container:aaa'
|
'network_mode': 'service:aaa'
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
networks=None,
|
networks=None,
|
||||||
|
|
@ -407,7 +407,7 @@ class ProjectTest(unittest.TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
service = project.get_service('test')
|
service = project.get_service('test')
|
||||||
self.assertEqual(service.net.mode, 'container:' + container_name)
|
self.assertEqual(service.network_mode.mode, 'container:' + container_name)
|
||||||
|
|
||||||
def test_uses_default_network_true(self):
|
def test_uses_default_network_true(self):
|
||||||
project = Project.from_config(
|
project = Project.from_config(
|
||||||
|
|
@ -426,7 +426,7 @@ class ProjectTest(unittest.TestCase):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert project.uses_default_network()
|
assert 'default' in project.networks.networks
|
||||||
|
|
||||||
def test_uses_default_network_false(self):
|
def test_uses_default_network_false(self):
|
||||||
project = Project.from_config(
|
project = Project.from_config(
|
||||||
|
|
@ -438,7 +438,7 @@ class ProjectTest(unittest.TestCase):
|
||||||
{
|
{
|
||||||
'name': 'foo',
|
'name': 'foo',
|
||||||
'image': 'busybox:latest',
|
'image': 'busybox:latest',
|
||||||
'networks': ['custom']
|
'networks': {'custom': None}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
networks={'custom': {}},
|
networks={'custom': {}},
|
||||||
|
|
@ -446,7 +446,7 @@ class ProjectTest(unittest.TestCase):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert not project.uses_default_network()
|
assert 'default' not in project.networks.networks
|
||||||
|
|
||||||
def test_container_without_name(self):
|
def test_container_without_name(self):
|
||||||
self.mock_client.containers.return_value = [
|
self.mock_client.containers.return_value = [
|
||||||
|
|
|
||||||
|
|
@ -15,16 +15,16 @@ from compose.const import LABEL_SERVICE
|
||||||
from compose.container import Container
|
from compose.container import Container
|
||||||
from compose.service import build_ulimits
|
from compose.service import build_ulimits
|
||||||
from compose.service import build_volume_binding
|
from compose.service import build_volume_binding
|
||||||
from compose.service import ContainerNet
|
from compose.service import ContainerNetworkMode
|
||||||
from compose.service import get_container_data_volumes
|
from compose.service import get_container_data_volumes
|
||||||
from compose.service import ImageType
|
from compose.service import ImageType
|
||||||
from compose.service import merge_volume_bindings
|
from compose.service import merge_volume_bindings
|
||||||
from compose.service import NeedsBuildError
|
from compose.service import NeedsBuildError
|
||||||
from compose.service import Net
|
from compose.service import NetworkMode
|
||||||
from compose.service import NoSuchImageError
|
from compose.service import NoSuchImageError
|
||||||
from compose.service import parse_repository_tag
|
from compose.service import parse_repository_tag
|
||||||
from compose.service import Service
|
from compose.service import Service
|
||||||
from compose.service import ServiceNet
|
from compose.service import ServiceNetworkMode
|
||||||
from compose.service import warn_on_masked_volume
|
from compose.service import warn_on_masked_volume
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -266,14 +266,53 @@ class ServiceTest(unittest.TestCase):
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
opts['labels'][LABEL_CONFIG_HASH],
|
opts['labels'][LABEL_CONFIG_HASH],
|
||||||
'3c85881a8903b9d73a06c41860c8be08acce1494ab4cf8408375966dccd714de')
|
'f8bfa1058ad1f4231372a0b1639f0dfdb574dafff4e8d7938049ae993f7cf1fc')
|
||||||
self.assertEqual(
|
assert opts['environment'] == ['also=real']
|
||||||
opts['environment'],
|
|
||||||
{
|
def test_get_container_create_options_sets_affinity_with_binds(self):
|
||||||
'affinity:container': '=ababab',
|
service = Service(
|
||||||
'also': 'real',
|
'foo',
|
||||||
}
|
image='foo',
|
||||||
|
client=self.mock_client,
|
||||||
)
|
)
|
||||||
|
self.mock_client.inspect_image.return_value = {'Id': 'abcd'}
|
||||||
|
prev_container = mock.Mock(
|
||||||
|
id='ababab',
|
||||||
|
image_config={'ContainerConfig': {'Volumes': ['/data']}})
|
||||||
|
|
||||||
|
def container_get(key):
|
||||||
|
return {
|
||||||
|
'Mounts': [
|
||||||
|
{
|
||||||
|
'Destination': '/data',
|
||||||
|
'Source': '/some/path',
|
||||||
|
'Name': 'abab1234',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}.get(key, None)
|
||||||
|
|
||||||
|
prev_container.get.side_effect = container_get
|
||||||
|
|
||||||
|
opts = service._get_container_create_options(
|
||||||
|
{},
|
||||||
|
1,
|
||||||
|
previous_container=prev_container)
|
||||||
|
|
||||||
|
assert opts['environment'] == ['affinity:container==ababab']
|
||||||
|
|
||||||
|
def test_get_container_create_options_no_affinity_without_binds(self):
|
||||||
|
service = Service('foo', image='foo', client=self.mock_client)
|
||||||
|
self.mock_client.inspect_image.return_value = {'Id': 'abcd'}
|
||||||
|
prev_container = mock.Mock(
|
||||||
|
id='ababab',
|
||||||
|
image_config={'ContainerConfig': {}})
|
||||||
|
prev_container.get.return_value = None
|
||||||
|
|
||||||
|
opts = service._get_container_create_options(
|
||||||
|
{},
|
||||||
|
1,
|
||||||
|
previous_container=prev_container)
|
||||||
|
assert opts['environment'] == []
|
||||||
|
|
||||||
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 = []
|
||||||
|
|
@ -407,7 +446,7 @@ class ServiceTest(unittest.TestCase):
|
||||||
'foo',
|
'foo',
|
||||||
image='example.com/foo',
|
image='example.com/foo',
|
||||||
client=self.mock_client,
|
client=self.mock_client,
|
||||||
net=ServiceNet(Service('other')),
|
network_mode=ServiceNetworkMode(Service('other')),
|
||||||
links=[(Service('one'), 'one')],
|
links=[(Service('one'), 'one')],
|
||||||
volumes_from=[VolumeFromSpec(Service('two'), 'rw', 'service')])
|
volumes_from=[VolumeFromSpec(Service('two'), 'rw', 'service')])
|
||||||
|
|
||||||
|
|
@ -417,11 +456,12 @@ class ServiceTest(unittest.TestCase):
|
||||||
'options': {'image': 'example.com/foo'},
|
'options': {'image': 'example.com/foo'},
|
||||||
'links': [('one', 'one')],
|
'links': [('one', 'one')],
|
||||||
'net': 'other',
|
'net': 'other',
|
||||||
|
'networks': [],
|
||||||
'volumes_from': [('two', 'rw')],
|
'volumes_from': [('two', 'rw')],
|
||||||
}
|
}
|
||||||
self.assertEqual(config_dict, expected)
|
assert config_dict == expected
|
||||||
|
|
||||||
def test_config_dict_with_net_from_container(self):
|
def test_config_dict_with_network_mode_from_container(self):
|
||||||
self.mock_client.inspect_image.return_value = {'Id': 'abcd'}
|
self.mock_client.inspect_image.return_value = {'Id': 'abcd'}
|
||||||
container = Container(
|
container = Container(
|
||||||
self.mock_client,
|
self.mock_client,
|
||||||
|
|
@ -430,17 +470,18 @@ class ServiceTest(unittest.TestCase):
|
||||||
'foo',
|
'foo',
|
||||||
image='example.com/foo',
|
image='example.com/foo',
|
||||||
client=self.mock_client,
|
client=self.mock_client,
|
||||||
net=container)
|
network_mode=ContainerNetworkMode(container))
|
||||||
|
|
||||||
config_dict = service.config_dict()
|
config_dict = service.config_dict()
|
||||||
expected = {
|
expected = {
|
||||||
'image_id': 'abcd',
|
'image_id': 'abcd',
|
||||||
'options': {'image': 'example.com/foo'},
|
'options': {'image': 'example.com/foo'},
|
||||||
'links': [],
|
'links': [],
|
||||||
|
'networks': [],
|
||||||
'net': 'aaabbb',
|
'net': 'aaabbb',
|
||||||
'volumes_from': [],
|
'volumes_from': [],
|
||||||
}
|
}
|
||||||
self.assertEqual(config_dict, expected)
|
assert config_dict == expected
|
||||||
|
|
||||||
def test_remove_image_none(self):
|
def test_remove_image_none(self):
|
||||||
web = Service('web', image='example', client=self.mock_client)
|
web = Service('web', image='example', client=self.mock_client)
|
||||||
|
|
@ -536,14 +577,6 @@ class ServiceTest(unittest.TestCase):
|
||||||
ports=["127.0.0.1:1000-2000:2000-3000"])
|
ports=["127.0.0.1:1000-2000:2000-3000"])
|
||||||
self.assertEqual(service.specifies_host_port(), True)
|
self.assertEqual(service.specifies_host_port(), True)
|
||||||
|
|
||||||
def test_get_links_with_networking(self):
|
|
||||||
service = Service(
|
|
||||||
'foo',
|
|
||||||
image='foo',
|
|
||||||
links=[(Service('one'), 'one')],
|
|
||||||
use_networking=True)
|
|
||||||
self.assertEqual(service._get_links(link_to_self=True), [])
|
|
||||||
|
|
||||||
def test_image_name_from_config(self):
|
def test_image_name_from_config(self):
|
||||||
image_name = 'example/web:latest'
|
image_name = 'example/web:latest'
|
||||||
service = Service('foo', image=image_name)
|
service = Service('foo', image=image_name)
|
||||||
|
|
@ -597,20 +630,20 @@ class BuildUlimitsTestCase(unittest.TestCase):
|
||||||
|
|
||||||
class NetTestCase(unittest.TestCase):
|
class NetTestCase(unittest.TestCase):
|
||||||
|
|
||||||
def test_net(self):
|
def test_network_mode(self):
|
||||||
net = Net('host')
|
network_mode = NetworkMode('host')
|
||||||
self.assertEqual(net.id, 'host')
|
self.assertEqual(network_mode.id, 'host')
|
||||||
self.assertEqual(net.mode, 'host')
|
self.assertEqual(network_mode.mode, 'host')
|
||||||
self.assertEqual(net.service_name, None)
|
self.assertEqual(network_mode.service_name, None)
|
||||||
|
|
||||||
def test_net_container(self):
|
def test_network_mode_container(self):
|
||||||
container_id = 'abcd'
|
container_id = 'abcd'
|
||||||
net = ContainerNet(Container(None, {'Id': container_id}))
|
network_mode = ContainerNetworkMode(Container(None, {'Id': container_id}))
|
||||||
self.assertEqual(net.id, container_id)
|
self.assertEqual(network_mode.id, container_id)
|
||||||
self.assertEqual(net.mode, 'container:' + container_id)
|
self.assertEqual(network_mode.mode, 'container:' + container_id)
|
||||||
self.assertEqual(net.service_name, None)
|
self.assertEqual(network_mode.service_name, None)
|
||||||
|
|
||||||
def test_net_service(self):
|
def test_network_mode_service(self):
|
||||||
container_id = 'bbbb'
|
container_id = 'bbbb'
|
||||||
service_name = 'web'
|
service_name = 'web'
|
||||||
mock_client = mock.create_autospec(docker.Client)
|
mock_client = mock.create_autospec(docker.Client)
|
||||||
|
|
@ -619,23 +652,23 @@ class NetTestCase(unittest.TestCase):
|
||||||
]
|
]
|
||||||
|
|
||||||
service = Service(name=service_name, client=mock_client)
|
service = Service(name=service_name, client=mock_client)
|
||||||
net = ServiceNet(service)
|
network_mode = ServiceNetworkMode(service)
|
||||||
|
|
||||||
self.assertEqual(net.id, service_name)
|
self.assertEqual(network_mode.id, service_name)
|
||||||
self.assertEqual(net.mode, 'container:' + container_id)
|
self.assertEqual(network_mode.mode, 'container:' + container_id)
|
||||||
self.assertEqual(net.service_name, service_name)
|
self.assertEqual(network_mode.service_name, service_name)
|
||||||
|
|
||||||
def test_net_service_no_containers(self):
|
def test_network_mode_service_no_containers(self):
|
||||||
service_name = 'web'
|
service_name = 'web'
|
||||||
mock_client = mock.create_autospec(docker.Client)
|
mock_client = mock.create_autospec(docker.Client)
|
||||||
mock_client.containers.return_value = []
|
mock_client.containers.return_value = []
|
||||||
|
|
||||||
service = Service(name=service_name, client=mock_client)
|
service = Service(name=service_name, client=mock_client)
|
||||||
net = ServiceNet(service)
|
network_mode = ServiceNetworkMode(service)
|
||||||
|
|
||||||
self.assertEqual(net.id, service_name)
|
self.assertEqual(network_mode.id, service_name)
|
||||||
self.assertEqual(net.mode, None)
|
self.assertEqual(network_mode.mode, None)
|
||||||
self.assertEqual(net.service_name, service_name)
|
self.assertEqual(network_mode.service_name, service_name)
|
||||||
|
|
||||||
|
|
||||||
def build_mount(destination, source, mode='rw'):
|
def build_mount(destination, source, mode='rw'):
|
||||||
|
|
@ -656,6 +689,7 @@ class ServiceVolumesTest(unittest.TestCase):
|
||||||
'/host/volume:/host/volume:ro',
|
'/host/volume:/host/volume:ro',
|
||||||
'/new/volume',
|
'/new/volume',
|
||||||
'/existing/volume',
|
'/existing/volume',
|
||||||
|
'named:/named/vol',
|
||||||
]]
|
]]
|
||||||
|
|
||||||
self.mock_client.inspect_image.return_value = {
|
self.mock_client.inspect_image.return_value = {
|
||||||
|
|
@ -697,8 +731,8 @@ class ServiceVolumesTest(unittest.TestCase):
|
||||||
}, has_been_inspected=True)
|
}, has_been_inspected=True)
|
||||||
|
|
||||||
expected = [
|
expected = [
|
||||||
VolumeSpec.parse('/var/lib/docker/aaaaaaaa:/existing/volume:rw'),
|
VolumeSpec.parse('existingvolume:/existing/volume:rw'),
|
||||||
VolumeSpec.parse('/var/lib/docker/cccccccc:/mnt/image/data:rw'),
|
VolumeSpec.parse('imagedata:/mnt/image/data:rw'),
|
||||||
]
|
]
|
||||||
|
|
||||||
volumes = get_container_data_volumes(container, options)
|
volumes = get_container_data_volumes(container, options)
|
||||||
|
|
@ -716,7 +750,8 @@ class ServiceVolumesTest(unittest.TestCase):
|
||||||
'ContainerConfig': {'Volumes': {}}
|
'ContainerConfig': {'Volumes': {}}
|
||||||
}
|
}
|
||||||
|
|
||||||
intermediate_container = Container(self.mock_client, {
|
previous_container = Container(self.mock_client, {
|
||||||
|
'Id': 'cdefab',
|
||||||
'Image': 'ababab',
|
'Image': 'ababab',
|
||||||
'Mounts': [{
|
'Mounts': [{
|
||||||
'Source': '/var/lib/docker/aaaaaaaa',
|
'Source': '/var/lib/docker/aaaaaaaa',
|
||||||
|
|
@ -730,11 +765,12 @@ class ServiceVolumesTest(unittest.TestCase):
|
||||||
expected = [
|
expected = [
|
||||||
'/host/volume:/host/volume:ro',
|
'/host/volume:/host/volume:ro',
|
||||||
'/host/rw/volume:/host/rw/volume:rw',
|
'/host/rw/volume:/host/rw/volume:rw',
|
||||||
'/var/lib/docker/aaaaaaaa:/existing/volume:rw',
|
'existingvolume:/existing/volume:rw',
|
||||||
]
|
]
|
||||||
|
|
||||||
binds = merge_volume_bindings(options, intermediate_container)
|
binds, affinity = merge_volume_bindings(options, previous_container)
|
||||||
self.assertEqual(set(binds), set(expected))
|
assert sorted(binds) == sorted(expected)
|
||||||
|
assert affinity == {'affinity:container': '=cdefab'}
|
||||||
|
|
||||||
def test_mount_same_host_path_to_two_volumes(self):
|
def test_mount_same_host_path_to_two_volumes(self):
|
||||||
service = Service(
|
service = Service(
|
||||||
|
|
@ -767,13 +803,14 @@ class ServiceVolumesTest(unittest.TestCase):
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_different_host_path_in_container_json(self):
|
def test_get_container_create_options_with_different_host_path_in_container_json(self):
|
||||||
service = Service(
|
service = Service(
|
||||||
'web',
|
'web',
|
||||||
image='busybox',
|
image='busybox',
|
||||||
volumes=[VolumeSpec.parse('/host/path:/data')],
|
volumes=[VolumeSpec.parse('/host/path:/data')],
|
||||||
client=self.mock_client,
|
client=self.mock_client,
|
||||||
)
|
)
|
||||||
|
volume_name = 'abcdefff1234'
|
||||||
|
|
||||||
self.mock_client.inspect_image.return_value = {
|
self.mock_client.inspect_image.return_value = {
|
||||||
'Id': 'ababab',
|
'Id': 'ababab',
|
||||||
|
|
@ -794,7 +831,7 @@ class ServiceVolumesTest(unittest.TestCase):
|
||||||
'Mode': '',
|
'Mode': '',
|
||||||
'RW': True,
|
'RW': True,
|
||||||
'Driver': 'local',
|
'Driver': 'local',
|
||||||
'Name': 'abcdefff1234'
|
'Name': volume_name,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -805,9 +842,9 @@ class ServiceVolumesTest(unittest.TestCase):
|
||||||
previous_container=Container(self.mock_client, {'Id': '123123123'}),
|
previous_container=Container(self.mock_client, {'Id': '123123123'}),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(
|
assert (
|
||||||
self.mock_client.create_host_config.call_args[1]['binds'],
|
self.mock_client.create_host_config.call_args[1]['binds'] ==
|
||||||
['/mnt/sda1/host/path:/data:rw'],
|
['{}:/data:rw'.format(volume_name)]
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_warn_on_masked_volume_no_warning_when_no_container_volumes(self):
|
def test_warn_on_masked_volume_no_warning_when_no_container_volumes(self):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue