Compare commits
289 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
45 changed files with 2005 additions and 546 deletions
117
CHANGELOG.md
117
CHANGELOG.md
|
|
@ -1,6 +1,123 @@
|
|||
Change log
|
||||
==========
|
||||
|
||||
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,4 +1,4 @@
|
|||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
|
||||
__version__ = '1.6.0dev'
|
||||
__version__ = '1.6.0rc2'
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ import logging
|
|||
import os
|
||||
|
||||
from docker import Client
|
||||
from docker.errors import TLSParameterError
|
||||
from docker.utils import kwargs_from_env
|
||||
|
||||
from ..const import HTTP_TIMEOUT
|
||||
from .errors import UserError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -20,8 +22,16 @@ def docker_client(version=None):
|
|||
if 'DOCKER_CLIENT_TIMEOUT' in os.environ:
|
||||
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:
|
||||
kwargs['version'] = version
|
||||
|
||||
kwargs['timeout'] = HTTP_TIMEOUT
|
||||
|
||||
return Client(**kwargs)
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ from .utils import yesno
|
|||
|
||||
|
||||
if not IS_WINDOWS_PLATFORM:
|
||||
import dockerpty
|
||||
from dockerpty.pty import PseudoTerminal
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
console_handler = logging.StreamHandler(sys.stderr)
|
||||
|
|
@ -709,8 +709,15 @@ def run_one_off_container(container_options, project, service, options):
|
|||
signals.set_signal_handler_to_shutdown()
|
||||
try:
|
||||
try:
|
||||
dockerpty.start(project.client, container.id, interactive=not options['-T'])
|
||||
service.connect_container_to_networks(container)
|
||||
pty = PseudoTerminal(
|
||||
project.client,
|
||||
container.id,
|
||||
interactive=not options['-T'],
|
||||
logs=False,
|
||||
)
|
||||
sockets = pty.sockets()
|
||||
service.start_container(container)
|
||||
pty.start(sockets)
|
||||
exit_code = container.wait()
|
||||
except signals.ShutdownException:
|
||||
project.client.stop(container.id)
|
||||
|
|
|
|||
|
|
@ -19,17 +19,22 @@ from .errors import CircularReference
|
|||
from .errors import ComposeFileNotFound
|
||||
from .errors import ConfigurationError
|
||||
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 .types import parse_extra_hosts
|
||||
from .types import parse_restart_spec
|
||||
from .types import VolumeFromSpec
|
||||
from .types import VolumeSpec
|
||||
from .validation import match_named_volumes
|
||||
from .validation import validate_against_fields_schema
|
||||
from .validation import validate_against_service_schema
|
||||
from .validation import validate_depends_on
|
||||
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_service_objects
|
||||
from .validation import validate_ulimits
|
||||
|
||||
|
||||
DOCKER_CONFIG_KEYS = [
|
||||
|
|
@ -134,6 +139,9 @@ class ConfigFile(namedtuple('_ConfigFile', 'filename config')):
|
|||
return 1
|
||||
return version
|
||||
|
||||
def get_service(self, name):
|
||||
return self.get_service_dicts()[name]
|
||||
|
||||
def get_service_dicts(self):
|
||||
return self.config if self.version == 1 else self.config.get('services', {})
|
||||
|
||||
|
|
@ -188,10 +196,10 @@ def find(base_dir, filenames):
|
|||
[ConfigFile.from_filename(f) for f in filenames])
|
||||
|
||||
|
||||
def validate_config_version(config_details):
|
||||
main_file = config_details.config_files[0]
|
||||
def validate_config_version(config_files):
|
||||
main_file = config_files[0]
|
||||
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)
|
||||
|
||||
if main_file.version != next_file.version:
|
||||
|
|
@ -254,7 +262,7 @@ def load(config_details):
|
|||
|
||||
Return a fully interpolated, extended and validated configuration.
|
||||
"""
|
||||
validate_config_version(config_details)
|
||||
validate_config_version(config_details.config_files)
|
||||
|
||||
processed_files = [
|
||||
process_config_file(config_file)
|
||||
|
|
@ -267,9 +275,13 @@ def load(config_details):
|
|||
networks = load_mapping(config_details.config_files, 'get_networks', 'Network')
|
||||
service_dicts = load_services(
|
||||
config_details.working_dir,
|
||||
main_file.filename,
|
||||
[file.get_service_dicts() for file in config_details.config_files],
|
||||
main_file.version)
|
||||
main_file,
|
||||
[file.get_service_dicts() for file in config_details.config_files])
|
||||
|
||||
if main_file.version >= 2:
|
||||
for service_dict in service_dicts:
|
||||
match_named_volumes(service_dict, volumes)
|
||||
|
||||
return Config(main_file.version, service_dicts, volumes, networks)
|
||||
|
||||
|
||||
|
|
@ -303,21 +315,22 @@ def load_mapping(config_files, get_func, entity_type):
|
|||
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):
|
||||
service_config = ServiceConfig.with_abs_paths(
|
||||
working_dir,
|
||||
filename,
|
||||
config_file.filename,
|
||||
service_name,
|
||||
service_dict)
|
||||
resolver = ServiceExtendsResolver(service_config, version)
|
||||
resolver = ServiceExtendsResolver(service_config, config_file)
|
||||
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_config._replace(config=service_dict),
|
||||
service_config,
|
||||
service_names,
|
||||
version)
|
||||
config_file.version)
|
||||
return service_dict
|
||||
|
||||
def build_services(service_config):
|
||||
|
|
@ -333,7 +346,7 @@ def load_services(working_dir, filename, service_configs, version):
|
|||
name: merge_service_dicts_from_files(
|
||||
base.get(name, {}),
|
||||
override.get(name, {}),
|
||||
version)
|
||||
config_file.version)
|
||||
for name in all_service_names
|
||||
}
|
||||
|
||||
|
|
@ -352,19 +365,19 @@ def process_config_file(config_file, service_name=None):
|
|||
|
||||
if config_file.version == 2:
|
||||
processed_config = dict(config_file.config)
|
||||
processed_config['services'] = interpolated_config
|
||||
processed_config['services'] = services = interpolated_config
|
||||
processed_config['volumes'] = interpolate_environment_variables(
|
||||
config_file.get_volumes(), 'volume')
|
||||
processed_config['networks'] = interpolate_environment_variables(
|
||||
config_file.get_networks(), 'network')
|
||||
|
||||
if config_file.version == 1:
|
||||
processed_config = interpolated_config
|
||||
processed_config = services = interpolated_config
|
||||
|
||||
config_file = config_file._replace(config=processed_config)
|
||||
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(
|
||||
"Cannot extend service '{}' in {}: Service not found".format(
|
||||
service_name, config_file.filename))
|
||||
|
|
@ -373,11 +386,11 @@ def process_config_file(config_file, service_name=None):
|
|||
|
||||
|
||||
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.working_dir = service_config.working_dir
|
||||
self.already_seen = already_seen or []
|
||||
self.version = version
|
||||
self.config_file = config_file
|
||||
|
||||
@property
|
||||
def signature(self):
|
||||
|
|
@ -404,10 +417,13 @@ class ServiceExtendsResolver(object):
|
|||
config_path = self.get_extended_config_path(extends)
|
||||
service_name = extends['service']
|
||||
|
||||
extends_file = ConfigFile.from_filename(config_path)
|
||||
validate_config_version([self.config_file, extends_file])
|
||||
extended_file = process_config_file(
|
||||
ConfigFile.from_filename(config_path),
|
||||
extends_file,
|
||||
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
|
||||
|
||||
def resolve_extends(self, extended_config_path, service_dict, service_name):
|
||||
|
|
@ -417,7 +433,7 @@ class ServiceExtendsResolver(object):
|
|||
extended_config_path,
|
||||
service_name,
|
||||
service_dict),
|
||||
self.version,
|
||||
self.config_file,
|
||||
already_seen=self.already_seen + [self.signature])
|
||||
|
||||
service_config = resolver.run()
|
||||
|
|
@ -425,13 +441,12 @@ class ServiceExtendsResolver(object):
|
|||
validate_extended_service_dict(
|
||||
other_service_dict,
|
||||
extended_config_path,
|
||||
service_name,
|
||||
)
|
||||
service_name)
|
||||
|
||||
return merge_service_dicts(
|
||||
other_service_dict,
|
||||
self.service_config.config,
|
||||
self.version)
|
||||
self.config_file.version)
|
||||
|
||||
def get_extended_config_path(self, extends_options):
|
||||
"""Service we are extending either has a value for 'file' set, which we
|
||||
|
|
@ -477,26 +492,28 @@ def validate_extended_service_dict(service_dict, filename, service):
|
|||
"%s services with 'volumes_from' cannot be extended" % error_prefix)
|
||||
|
||||
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(
|
||||
"%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):
|
||||
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(
|
||||
"ulimit_config \"{}\" cannot contain a 'soft' value higher "
|
||||
"than 'hard' value".format(ulimit_config))
|
||||
if 'depends_on' in service_dict:
|
||||
raise ConfigurationError(
|
||||
"%s services with 'depends_on' cannot be extended" % error_prefix)
|
||||
|
||||
|
||||
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_paths(service_dict)
|
||||
|
||||
if 'ulimits' in service_dict:
|
||||
validate_ulimits(service_dict['ulimits'])
|
||||
validate_ulimits(service_config)
|
||||
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):
|
||||
raise ConfigurationError(
|
||||
|
|
@ -556,6 +573,14 @@ def finalize_service(service_config, service_names, version):
|
|||
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 'restart' in service_dict:
|
||||
service_dict['restart'] = parse_restart_spec(service_dict['restart'])
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,34 @@
|
|||
"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": {
|
||||
"id": "#/definitions/volume",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
"cpu_shares": {"type": ["number", "string"]},
|
||||
"cpu_quota": {"type": ["number", "string"]},
|
||||
"cpuset": {"type": "string"},
|
||||
"depends_on": {"$ref": "#/definitions/list_of_strings"},
|
||||
"devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||
"dns": {"$ref": "#/definitions/string_or_list"},
|
||||
"dns_search": {"$ref": "#/definitions/string_or_list"},
|
||||
|
|
@ -82,11 +83,13 @@
|
|||
]
|
||||
},
|
||||
|
||||
"external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||
"extra_hosts": {"$ref": "#/definitions/list_or_dict"},
|
||||
"hostname": {"type": "string"},
|
||||
"image": {"type": "string"},
|
||||
"ipc": {"type": "string"},
|
||||
"labels": {"$ref": "#/definitions/list_or_dict"},
|
||||
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||
|
||||
"logging": {
|
||||
"type": "object",
|
||||
|
|
@ -101,6 +104,7 @@
|
|||
"mac_address": {"type": "string"},
|
||||
"mem_limit": {"type": ["number", "string"]},
|
||||
"memswap_limit": {"type": ["number", "string"]},
|
||||
"network_mode": {"type": "string"},
|
||||
|
||||
"networks": {
|
||||
"type": "array",
|
||||
|
|
|
|||
|
|
@ -4,14 +4,22 @@ from __future__ import unicode_literals
|
|||
from compose.config.errors import DependencyError
|
||||
|
||||
|
||||
def get_service_name_from_net(net_config):
|
||||
if not net_config:
|
||||
def get_service_name_from_network_mode(network_mode):
|
||||
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
|
||||
|
||||
if not net_config.startswith('container:'):
|
||||
if not network_mode.startswith(source_type+':'):
|
||||
return
|
||||
|
||||
_, net_name = net_config.split(':', 1)
|
||||
_, net_name = network_mode.split(':', 1)
|
||||
return net_name
|
||||
|
||||
|
||||
|
|
@ -33,7 +41,8 @@ def sort_service_dicts(services):
|
|||
service for service in services
|
||||
if (name in get_service_names(service.get('links', [])) or
|
||||
name in get_service_names_from_volumes_from(service.get('volumes_from', [])) or
|
||||
name == get_service_name_from_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):
|
||||
|
|
@ -42,8 +51,10 @@ def sort_service_dicts(services):
|
|||
raise DependencyError('A service can not link to itself: %s' % n['name'])
|
||||
if n['name'] in n.get('volumes_from', []):
|
||||
raise DependencyError('A service can not mount itself as volume: %s' % n['name'])
|
||||
else:
|
||||
raise DependencyError('Circular import between %s' % ' and '.join(temporary_marked))
|
||||
if n['name'] in n.get('depends_on', []):
|
||||
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:
|
||||
temporary_marked.add(n['name'])
|
||||
for m in get_service_dependents(n, services):
|
||||
|
|
|
|||
|
|
@ -163,3 +163,7 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')):
|
|||
def repr(self):
|
||||
external = self.external + ':' if self.external else ''
|
||||
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(('.', '/', '~'))
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from jsonschema import RefResolver
|
|||
from jsonschema import ValidationError
|
||||
|
||||
from .errors import ConfigurationError
|
||||
from .sort_services import get_service_name_from_network_mode
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
@ -77,6 +78,18 @@ def format_boolean_in_environment(instance):
|
|||
return True
|
||||
|
||||
|
||||
def match_named_volumes(service_dict, project_volumes):
|
||||
service_volumes = service_dict.get('volumes', [])
|
||||
for volume_spec in service_volumes:
|
||||
if volume_spec.is_named_volume and volume_spec.external not in project_volumes:
|
||||
raise ConfigurationError(
|
||||
'Named volume "{0}" is used in service "{1}" but no'
|
||||
' declaration was found in the volumes section.'.format(
|
||||
volume_spec.repr(), service_dict.get('name')
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def validate_top_level_service_objects(filename, service_dicts):
|
||||
"""Perform some high level validation of the service name and value.
|
||||
|
||||
|
|
@ -110,6 +123,18 @@ def validate_top_level_object(config_file):
|
|||
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):
|
||||
"""
|
||||
The service to be extended must either be defined in the config key 'file',
|
||||
|
|
@ -123,6 +148,32 @@ def validate_extends_file_path(service_name, extends_options, filename):
|
|||
)
|
||||
|
||||
|
||||
def validate_network_mode(service_config, service_names):
|
||||
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(service_name, error_key):
|
||||
msg = "Unsupported config option for '{}' service: '{}'".format(service_name, error_key)
|
||||
if error_key in DOCKER_CONFIG_HINTS:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ from __future__ import unicode_literals
|
|||
import logging
|
||||
|
||||
from docker.errors import NotFound
|
||||
from docker.utils import create_ipam_config
|
||||
from docker.utils import create_ipam_pool
|
||||
|
||||
from .config import ConfigurationError
|
||||
|
||||
|
|
@ -13,12 +15,13 @@ log = logging.getLogger(__name__)
|
|||
|
||||
class Network(object):
|
||||
def __init__(self, client, project, name, driver=None, driver_opts=None,
|
||||
external_name=None):
|
||||
ipam=None, external_name=None):
|
||||
self.client = client
|
||||
self.project = project
|
||||
self.name = name
|
||||
self.driver = driver
|
||||
self.driver_opts = driver_opts
|
||||
self.ipam = create_ipam_config_from_dict(ipam)
|
||||
self.external_name = external_name
|
||||
|
||||
def ensure(self):
|
||||
|
|
@ -44,11 +47,11 @@ class Network(object):
|
|||
data = self.inspect()
|
||||
if self.driver and data['Driver'] != self.driver:
|
||||
raise ConfigurationError(
|
||||
'Network {} needs to be recreated - driver has changed'
|
||||
'Network "{}" needs to be recreated - driver has changed'
|
||||
.format(self.full_name))
|
||||
if data['Options'] != (self.driver_opts or {}):
|
||||
raise ConfigurationError(
|
||||
'Network {} needs to be recreated - options have changed'
|
||||
'Network "{}" needs to be recreated - options have changed'
|
||||
.format(self.full_name))
|
||||
except NotFound:
|
||||
driver_name = 'the default driver'
|
||||
|
|
@ -61,7 +64,10 @@ class Network(object):
|
|||
)
|
||||
|
||||
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):
|
||||
|
|
@ -80,3 +86,21 @@ class Network(object):
|
|||
if self.external_name:
|
||||
return self.external_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', [])
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ from docker.errors import NotFound
|
|||
|
||||
from . import parallel
|
||||
from .config import ConfigurationError
|
||||
from .config.sort_services import get_service_name_from_net
|
||||
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 IMAGE_EVENTS
|
||||
from .const import LABEL_ONE_OFF
|
||||
|
|
@ -18,11 +19,11 @@ from .const import LABEL_PROJECT
|
|||
from .const import LABEL_SERVICE
|
||||
from .container import Container
|
||||
from .network import Network
|
||||
from .service import ContainerNet
|
||||
from .service import ContainerNetworkMode
|
||||
from .service import ConvergenceStrategy
|
||||
from .service import Net
|
||||
from .service import NetworkMode
|
||||
from .service import Service
|
||||
from .service import ServiceNet
|
||||
from .service import ServiceNetworkMode
|
||||
from .utils import microseconds_from_time_nano
|
||||
from .volume import Volume
|
||||
|
||||
|
|
@ -42,7 +43,7 @@ class Project(object):
|
|||
self.use_networking = use_networking
|
||||
self.network_driver = network_driver
|
||||
self.networks = networks or []
|
||||
self.volumes = volumes or []
|
||||
self.volumes = volumes or {}
|
||||
|
||||
def labels(self, one_off=False):
|
||||
return [
|
||||
|
|
@ -58,32 +59,50 @@ class Project(object):
|
|||
use_networking = (config_data.version and config_data.version >= 2)
|
||||
project = cls(name, [], client, use_networking=use_networking)
|
||||
|
||||
custom_networks = []
|
||||
if config_data.networks:
|
||||
for network_name, data in config_data.networks.items():
|
||||
custom_networks.append(
|
||||
Network(
|
||||
client=client, project=name, name=network_name,
|
||||
driver=data.get('driver'),
|
||||
driver_opts=data.get('driver_opts'),
|
||||
external_name=data.get('external_name'),
|
||||
)
|
||||
network_config = config_data.networks or {}
|
||||
custom_networks = [
|
||||
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()
|
||||
]
|
||||
|
||||
all_networks = custom_networks[:]
|
||||
if 'default' not in network_config:
|
||||
all_networks.append(project.default_network)
|
||||
|
||||
if config_data.volumes:
|
||||
for vol_name, data in config_data.volumes.items():
|
||||
project.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 service_dict in config_data.services:
|
||||
if use_networking:
|
||||
networks = get_networks(
|
||||
service_dict,
|
||||
custom_networks + [project.default_network])
|
||||
net = Net(networks[0]) if networks else Net("none")
|
||||
links = []
|
||||
networks = get_networks(service_dict, all_networks)
|
||||
else:
|
||||
networks = []
|
||||
net = project.get_net(service_dict)
|
||||
links = project.get_links(service_dict)
|
||||
|
||||
links = project.get_links(service_dict)
|
||||
network_mode = project.get_network_mode(service_dict, networks)
|
||||
volumes_from = get_volumes_from(project, service_dict)
|
||||
|
||||
if config_data.version == 2:
|
||||
service_volumes = service_dict.get('volumes', [])
|
||||
for volume_spec in service_volumes:
|
||||
if volume_spec.is_named_volume:
|
||||
declared_volume = project.volumes[volume_spec.external]
|
||||
service_volumes[service_volumes.index(volume_spec)] = (
|
||||
volume_spec._replace(external=declared_volume.full_name)
|
||||
)
|
||||
|
||||
project.services.append(
|
||||
Service(
|
||||
client=client,
|
||||
|
|
@ -91,25 +110,15 @@ class Project(object):
|
|||
use_networking=use_networking,
|
||||
networks=networks,
|
||||
links=links,
|
||||
net=net,
|
||||
network_mode=network_mode,
|
||||
volumes_from=volumes_from,
|
||||
**service_dict))
|
||||
**service_dict)
|
||||
)
|
||||
|
||||
project.networks += custom_networks
|
||||
if project.uses_default_network():
|
||||
if 'default' not in network_config and 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
|
||||
|
||||
@property
|
||||
|
|
@ -188,27 +197,27 @@ class Project(object):
|
|||
del service_dict['links']
|
||||
return links
|
||||
|
||||
def get_net(self, service_dict):
|
||||
net = service_dict.pop('net', None)
|
||||
if not net:
|
||||
return Net(None)
|
||||
def get_network_mode(self, service_dict, networks):
|
||||
network_mode = service_dict.pop('network_mode', None)
|
||||
if not network_mode:
|
||||
if self.use_networking:
|
||||
return NetworkMode(networks[0]) if networks else NetworkMode('none')
|
||||
return NetworkMode(None)
|
||||
|
||||
net_name = get_service_name_from_net(net)
|
||||
if not net_name:
|
||||
return Net(net)
|
||||
service_name = get_service_name_from_network_mode(network_mode)
|
||||
if service_name:
|
||||
return ServiceNetworkMode(self.get_service(service_name))
|
||||
|
||||
try:
|
||||
return ServiceNet(self.get_service(net_name))
|
||||
except NoSuchService:
|
||||
pass
|
||||
try:
|
||||
return ContainerNet(Container.from_id(self.client, net_name))
|
||||
except APIError:
|
||||
raise ConfigurationError(
|
||||
'Service "%s" is trying to use the network of "%s", '
|
||||
'which is not the name of a service or container.' % (
|
||||
service_dict['name'],
|
||||
net_name))
|
||||
container_name = get_container_name_from_network_mode(network_mode)
|
||||
if container_name:
|
||||
try:
|
||||
return ContainerNetworkMode(Container.from_id(self.client, container_name))
|
||||
except APIError:
|
||||
raise ConfigurationError(
|
||||
"Service '{name}' uses the network stack of container '{dep}' which "
|
||||
"does not exist.".format(name=service_dict['name'], dep=container_name))
|
||||
|
||||
return NetworkMode(network_mode)
|
||||
|
||||
def start(self, service_names=None, **options):
|
||||
containers = []
|
||||
|
|
@ -238,7 +247,7 @@ class Project(object):
|
|||
|
||||
def initialize_volumes(self):
|
||||
try:
|
||||
for volume in self.volumes:
|
||||
for volume in self.volumes.values():
|
||||
if volume.external:
|
||||
log.debug(
|
||||
'Volume {0} declared as external. No new '
|
||||
|
|
@ -293,7 +302,7 @@ class Project(object):
|
|||
network.remove()
|
||||
|
||||
def remove_volumes(self):
|
||||
for volume in self.volumes:
|
||||
for volume in self.volumes.values():
|
||||
volume.remove()
|
||||
|
||||
def initialize_networks(self):
|
||||
|
|
@ -456,18 +465,18 @@ class Project(object):
|
|||
|
||||
|
||||
def get_networks(service_dict, network_definitions):
|
||||
if 'network_mode' in service_dict:
|
||||
return []
|
||||
|
||||
networks = []
|
||||
for name in service_dict.pop('networks', ['default']):
|
||||
if name in ['bridge', 'host']:
|
||||
networks.append(name)
|
||||
matches = [n for n in network_definitions if n.name == name]
|
||||
if matches:
|
||||
networks.append(matches[0].full_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))
|
||||
raise ConfigurationError(
|
||||
'Service "{}" uses an undefined network "{}"'
|
||||
.format(service_dict['name'], name))
|
||||
return networks
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -27,9 +27,7 @@ from .const import LABEL_SERVICE
|
|||
from .const import LABEL_VERSION
|
||||
from .container import Container
|
||||
from .parallel import parallel_execute
|
||||
from .parallel import parallel_remove
|
||||
from .parallel import parallel_start
|
||||
from .parallel import parallel_stop
|
||||
from .progress_stream import stream_output
|
||||
from .progress_stream import StreamOutputError
|
||||
from .utils import json_hash
|
||||
|
|
@ -49,7 +47,6 @@ DOCKER_START_KEYS = [
|
|||
'extra_hosts',
|
||||
'ipc',
|
||||
'read_only',
|
||||
'net',
|
||||
'log_driver',
|
||||
'log_opt',
|
||||
'mem_limit',
|
||||
|
|
@ -115,7 +112,7 @@ class Service(object):
|
|||
use_networking=False,
|
||||
links=None,
|
||||
volumes_from=None,
|
||||
net=None,
|
||||
network_mode=None,
|
||||
networks=None,
|
||||
**options
|
||||
):
|
||||
|
|
@ -125,7 +122,7 @@ class Service(object):
|
|||
self.use_networking = use_networking
|
||||
self.links = links 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.options = options
|
||||
|
||||
|
|
@ -180,6 +177,10 @@ class Service(object):
|
|||
service.start_container(container)
|
||||
return container
|
||||
|
||||
def stop_and_remove(container):
|
||||
container.stop(timeout=timeout)
|
||||
container.remove()
|
||||
|
||||
running_containers = self.containers(stopped=False)
|
||||
num_running = len(running_containers)
|
||||
|
||||
|
|
@ -225,14 +226,17 @@ class Service(object):
|
|||
|
||||
if desired_num < num_running:
|
||||
num_to_stop = num_running - desired_num
|
||||
|
||||
sorted_running_containers = sorted(
|
||||
running_containers,
|
||||
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,
|
||||
one_off=False,
|
||||
|
|
@ -425,11 +429,18 @@ class Service(object):
|
|||
return container
|
||||
|
||||
def connect_container_to_networks(self, container):
|
||||
connected_networks = container.get('NetworkSettings.Networks')
|
||||
|
||||
for network in self.networks:
|
||||
log.debug('Connecting "{}" to "{}"'.format(container.name, network))
|
||||
if network in connected_networks:
|
||||
self.client.disconnect_container_from_network(
|
||||
container.id, network)
|
||||
|
||||
self.client.connect_container_to_network(
|
||||
container.id, network,
|
||||
aliases=[self.name])
|
||||
aliases=self._get_aliases(container),
|
||||
links=self._get_links(False),
|
||||
)
|
||||
|
||||
def remove_duplicate_containers(self, timeout=DEFAULT_TIMEOUT):
|
||||
for c in self.duplicate_containers():
|
||||
|
|
@ -460,7 +471,7 @@ class Service(object):
|
|||
'options': self.options,
|
||||
'image_id': self.image()['Id'],
|
||||
'links': self.get_link_names(),
|
||||
'net': self.net.id,
|
||||
'net': self.network_mode.id,
|
||||
'volumes_from': [
|
||||
(v.source.name, v.mode)
|
||||
for v in self.volumes_from if isinstance(v.source, Service)
|
||||
|
|
@ -468,10 +479,11 @@ class Service(object):
|
|||
}
|
||||
|
||||
def get_dependency_names(self):
|
||||
net_name = self.net.service_name
|
||||
net_name = self.network_mode.service_name
|
||||
return (self.get_linked_service_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):
|
||||
return [service.name for (service, _) in self.links]
|
||||
|
|
@ -498,28 +510,38 @@ class Service(object):
|
|||
numbers = [c.number for c in containers]
|
||||
return 1 if not numbers else max(numbers) + 1
|
||||
|
||||
def _get_links(self, link_to_self):
|
||||
if self.use_networking:
|
||||
def _get_aliases(self, container):
|
||||
if container.labels.get(LABEL_ONE_OFF) == "True":
|
||||
return []
|
||||
|
||||
links = []
|
||||
return [self.name, container.short_id]
|
||||
|
||||
def _get_links(self, link_to_self):
|
||||
links = {}
|
||||
|
||||
for service, link_name in self.links:
|
||||
for container in service.containers():
|
||||
links.append((container.name, link_name or service.name))
|
||||
links.append((container.name, container.name))
|
||||
links.append((container.name, container.name_without_project))
|
||||
links[link_name or service.name] = container.name
|
||||
links[container.name] = container.name
|
||||
links[container.name_without_project] = container.name
|
||||
|
||||
if link_to_self:
|
||||
for container in self.containers():
|
||||
links.append((container.name, self.name))
|
||||
links.append((container.name, container.name))
|
||||
links.append((container.name, container.name_without_project))
|
||||
links[self.name] = container.name
|
||||
links[container.name] = container.name
|
||||
links[container.name_without_project] = container.name
|
||||
|
||||
for external_link in self.options.get('external_links') or []:
|
||||
if ':' not in external_link:
|
||||
link_name = external_link
|
||||
else:
|
||||
external_link, link_name = external_link.split(':')
|
||||
links.append((external_link, link_name))
|
||||
return links
|
||||
links[link_name] = external_link
|
||||
|
||||
return [
|
||||
(alias, container_name)
|
||||
for (container_name, alias) in links.items()
|
||||
]
|
||||
|
||||
def _get_volumes_from(self):
|
||||
return [build_volume_from(spec) for spec in self.volumes_from]
|
||||
|
|
@ -599,8 +621,6 @@ class Service(object):
|
|||
override_options,
|
||||
one_off=one_off)
|
||||
|
||||
container_options['networking_config'] = self._get_container_networking_config()
|
||||
|
||||
return container_options
|
||||
|
||||
def _get_container_host_config(self, override_options, one_off=False):
|
||||
|
|
@ -615,7 +635,7 @@ class Service(object):
|
|||
binds=options.get('binds'),
|
||||
volumes_from=self._get_volumes_from(),
|
||||
privileged=options.get('privileged', False),
|
||||
network_mode=self.net.mode,
|
||||
network_mode=self.network_mode.mode,
|
||||
devices=options.get('devices'),
|
||||
dns=options.get('dns'),
|
||||
dns_search=options.get('dns_search'),
|
||||
|
|
@ -635,13 +655,6 @@ class Service(object):
|
|||
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):
|
||||
log.info('Building %s' % self.name)
|
||||
|
||||
|
|
@ -760,22 +773,22 @@ class Service(object):
|
|||
log.error(six.text_type(e))
|
||||
|
||||
|
||||
class Net(object):
|
||||
class NetworkMode(object):
|
||||
"""A `standard` network mode (ex: host, bridge)"""
|
||||
|
||||
service_name = None
|
||||
|
||||
def __init__(self, net):
|
||||
self.net = net
|
||||
def __init__(self, network_mode):
|
||||
self.network_mode = network_mode
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.net
|
||||
return self.network_mode
|
||||
|
||||
mode = id
|
||||
|
||||
|
||||
class ContainerNet(object):
|
||||
class ContainerNetworkMode(object):
|
||||
"""A network mode that uses a container's network stack."""
|
||||
|
||||
service_name = None
|
||||
|
|
@ -792,7 +805,7 @@ class ContainerNet(object):
|
|||
return 'container:' + self.container.id
|
||||
|
||||
|
||||
class ServiceNet(object):
|
||||
class ServiceNetworkMode(object):
|
||||
"""A network mode that uses a service's network stack."""
|
||||
|
||||
def __init__(self, service):
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ __docker-compose_commands() {
|
|||
local -a lines
|
||||
lines=(${(f)"$(_call_program commands docker-compose 2>&1)"})
|
||||
_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
|
||||
_describe -t docker-compose-commands "docker-compose command" _docker_compose_subcommands
|
||||
}
|
||||
|
|
@ -203,6 +203,12 @@ __docker-compose_subcommand() {
|
|||
'(--quiet -q)'{--quiet,-q}"[Only validate the configuration, don't print anything.]" \
|
||||
'--services[Print the service names, one per line.]' && 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)
|
||||
_arguments \
|
||||
$opts_help \
|
||||
|
|
@ -298,12 +304,13 @@ __docker-compose_subcommand() {
|
|||
(up)
|
||||
_arguments \
|
||||
$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-deps[Don't start linked services.]" \
|
||||
"--force-recreate[Recreate containers even if their configuration and image haven't changed. Incompatible with --no-recreate.]" \
|
||||
"--no-recreate[If containers already exist, don't recreate them.]" \
|
||||
"--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: " \
|
||||
'*:services:__docker-compose_services_all' && ret=0
|
||||
;;
|
||||
|
|
|
|||
99
contrib/migration/migrate-compose-file-v1-to-v2.py
Executable file
99
contrib/migration/migrate-compose-file-v1-to-v2.py
Executable file
|
|
@ -0,0 +1,99 @@
|
|||
#!/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
|
||||
|
||||
|
||||
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():
|
||||
# remove links and external links
|
||||
service.pop('links', None)
|
||||
external_links = service.pop('external_links', None)
|
||||
if external_links:
|
||||
log.warn(
|
||||
"Service {name} has external_links: {ext}, which are no longer "
|
||||
"supported. See https://docs.docker.com/compose/networking/ "
|
||||
"for options on how to connect external containers to the "
|
||||
"compose network.".format(name=name, ext=external_links))
|
||||
|
||||
# net is now networks
|
||||
if 'net' in service:
|
||||
service['networks'] = [service.pop('net')]
|
||||
|
||||
# create build section
|
||||
if 'dockerfile' in service:
|
||||
service['build'] = {
|
||||
'context': service.pop('build'),
|
||||
'dockerfile': service.pop('dockerfile'),
|
||||
}
|
||||
|
||||
# create logging section
|
||||
if 'log_driver' in service:
|
||||
service['logging'] = {'driver': service.pop('log_driver')}
|
||||
if 'log_opt' in service:
|
||||
service['logging']['options'] = service.pop('log_opt')
|
||||
|
||||
# volumes_from prefix with 'container:'
|
||||
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
|
||||
|
||||
data['services'] = {name: data.pop(name) for name in data.keys()}
|
||||
data['version'] = 2
|
||||
return data
|
||||
|
||||
|
||||
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()
|
||||
|
||||
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)
|
||||
|
|
@ -12,79 +12,33 @@ parent="smn_compose_ref"
|
|||
|
||||
# Compose file reference
|
||||
|
||||
The compose file is a [YAML](http://yaml.org/) file where all the top level
|
||||
keys are the name of a service, and the values are the service definition.
|
||||
The default path for a compose file is `./docker-compose.yml`.
|
||||
The Compose file is a [YAML](http://yaml.org/) file defining
|
||||
[services](#service-configuration-reference),
|
||||
[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
|
||||
`image` or `build`. Other keys are optional, and are analogous to their
|
||||
`docker run` command-line counterparts.
|
||||
A service definition contains configuration which will be applied to each
|
||||
container started for that service, much like passing command-line parameters to
|
||||
`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`,
|
||||
`EXPOSE`, `VOLUME`, `ENV`) are respected by default - you don't need to
|
||||
specify them again in `docker-compose.yml`.
|
||||
|
||||
## Versioning
|
||||
|
||||
It is possible to use different versions of the `compose.yml` format.
|
||||
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
|
||||
You can use environment variables in configuration values with a Bash-like
|
||||
`${VARIABLE}` syntax - see [variable substitution](#variable-substitution) for
|
||||
full details.
|
||||
|
||||
|
||||
## 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
|
||||
definition.
|
||||
|
||||
|
|
@ -92,28 +46,30 @@ definition.
|
|||
|
||||
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
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
|
||||
version: 2
|
||||
services:
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile-alternate
|
||||
args:
|
||||
buildno: 1
|
||||
> **Note**: In the [version 1 file format](#version-1), `build` is different in
|
||||
> two ways:
|
||||
>
|
||||
> - Only the string form (`build: .`) is allowed - not the object form.
|
||||
> - Using `build` together with `image` is not allowed. Attempting to do so
|
||||
> results in an error.
|
||||
|
||||
#### 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.
|
||||
|
||||
When the value supplied is a relative path, it is interpreted as relative to the
|
||||
|
|
@ -122,29 +78,34 @@ sent to the Docker daemon.
|
|||
|
||||
Compose will build and tag it with a generated name, and use that image thereafter.
|
||||
|
||||
build: /path/to/build/dir
|
||||
|
||||
build:
|
||||
context: /path/to/build/dir
|
||||
|
||||
Using `context` together with `image` is not allowed. Attempting to do so results in
|
||||
an error.
|
||||
context: ./dir
|
||||
|
||||
#### dockerfile
|
||||
|
||||
Alternate Dockerfile.
|
||||
|
||||
Compose will use an alternate file to build with. A build path must also be
|
||||
specified using the `build` key.
|
||||
specified.
|
||||
|
||||
build:
|
||||
context: /path/to/build/dir
|
||||
context: .
|
||||
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
|
||||
|
||||
> [Version 2 file format](#version-2) only.
|
||||
|
||||
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
|
||||
they are not converted to True or False by the YML parser.
|
||||
|
|
@ -152,8 +113,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
|
||||
machine Compose is running on.
|
||||
|
||||
> **Note:** Introduced in version 2 of the compose file format.
|
||||
|
||||
build:
|
||||
args:
|
||||
buildno: 1
|
||||
|
|
@ -210,6 +169,29 @@ client create option.
|
|||
devices:
|
||||
- "/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`.
|
||||
|
||||
version: 2
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
redis:
|
||||
image: redis
|
||||
db:
|
||||
image: postgres
|
||||
|
||||
### dns
|
||||
|
||||
Custom DNS servers. Can be a single value or a list.
|
||||
|
|
@ -336,6 +318,10 @@ container name and the link alias (`CONTAINER:ALIAS`).
|
|||
- project_db_1:mysql
|
||||
- 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
|
||||
|
||||
Add hostname mappings. Use the same values as the docker client `--add-host` parameter.
|
||||
|
|
@ -377,33 +363,35 @@ It's recommended that you use reverse-DNS notation to prevent your labels from c
|
|||
### links
|
||||
|
||||
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
|
||||
used for the alias).
|
||||
a link alias (`SERVICE:ALIAS`), or just the service name.
|
||||
|
||||
links:
|
||||
- db
|
||||
- db:database
|
||||
- redis
|
||||
web:
|
||||
links:
|
||||
- db
|
||||
- db:database
|
||||
- redis
|
||||
|
||||
An entry with the alias' name will be created in `/etc/hosts` inside containers
|
||||
for this service, e.g:
|
||||
Containers for the linked service will be reachable at a hostname identical to
|
||||
the alias, or the service name if no alias was specified.
|
||||
|
||||
172.17.2.186 db
|
||||
172.17.2.186 database
|
||||
172.17.2.187 redis
|
||||
Links also express dependency between services in the same way as
|
||||
[depends_on](#depends-on), so they determine the order of service startup.
|
||||
|
||||
Environment variables will also be created - see the [environment variable
|
||||
reference](env.md) for details.
|
||||
> **Note:** If you define both links and [networks](#networks), services with
|
||||
> links between them must share at least one network in common in order to
|
||||
> communicate.
|
||||
|
||||
### logging
|
||||
|
||||
Logging configuration for the service. This configuration replaces the previous
|
||||
`log_driver` and `log_opt` keys.
|
||||
> [Version 2 file format](#version-2) only. In version 1, use
|
||||
> [log_driver](#log_driver) and [log_opt](#log_opt).
|
||||
|
||||
Logging configuration for the service.
|
||||
|
||||
logging:
|
||||
driver: log_driver
|
||||
options:
|
||||
syslog-address: "tcp://192.168.0.42:123"
|
||||
driver: syslog
|
||||
options:
|
||||
syslog-address: "tcp://192.168.0.42:123"
|
||||
|
||||
The `driver` name specifies a logging driver for the service's
|
||||
containers, as with the ``--log-driver`` option for docker run
|
||||
|
|
@ -421,21 +409,68 @@ 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`.
|
||||
|
||||
|
||||
Logging options are key value pairs. An example of `syslog` options:
|
||||
Logging options are key-value pairs. An example of `syslog` options:
|
||||
|
||||
driver: "syslog"
|
||||
options:
|
||||
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
|
||||
|
||||
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: "none"
|
||||
net: "container:[name or id]"
|
||||
net: "host"
|
||||
net: "none"
|
||||
net: "container:[service name or container name/id]"
|
||||
|
||||
### network_mode
|
||||
|
||||
> [Version 2 file format](#version-1) 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
|
||||
|
||||
### pid
|
||||
|
||||
|
|
@ -469,9 +504,17 @@ port (a random host port will be chosen).
|
|||
|
||||
Override the default labeling scheme for each container.
|
||||
|
||||
security_opt:
|
||||
- label:user:USER
|
||||
- label:role:ROLE
|
||||
security_opt:
|
||||
- label:user:USER
|
||||
- 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
|
||||
|
||||
|
|
@ -479,32 +522,45 @@ Override the default ulimits for a container. You can either specify a single
|
|||
limit as an integer or soft/hard limits as a mapping.
|
||||
|
||||
|
||||
ulimits:
|
||||
nproc: 65535
|
||||
nofile:
|
||||
soft: 20000
|
||||
hard: 40000
|
||||
ulimits:
|
||||
nproc: 65535
|
||||
nofile:
|
||||
soft: 20000
|
||||
hard: 40000
|
||||
|
||||
### volumes, volume\_driver
|
||||
|
||||
Mount paths as volumes, optionally specifying a path on the host machine
|
||||
(`HOST:CONTAINER`), or an access mode (`HOST:CONTAINER:ro`).
|
||||
|
||||
volumes:
|
||||
- /var/lib/mysql
|
||||
- ./cache:/tmp/cache
|
||||
- ~/configs:/etc/configs/:ro
|
||||
Mount paths or named volumes, optionally specifying a path on the host machine
|
||||
(`HOST:CONTAINER`), or an access mode (`HOST:CONTAINER:ro`). Named volumes can
|
||||
be specified with the
|
||||
[top-level `volumes` key](#volume-configuration-reference), but this is
|
||||
optional - the Docker Engine will create the volume if it doesn't exist.
|
||||
|
||||
You can mount a relative path on the host, which will expand relative to
|
||||
the directory of the Compose configuration file being used. Relative paths
|
||||
should always begin with `.` or `..`.
|
||||
|
||||
volumes:
|
||||
# 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 use a volume name (instead of a volume path), you may also specify
|
||||
a `volume_driver`.
|
||||
|
||||
volume_driver: mydriver
|
||||
|
||||
|
||||
> Note: No path expansion will be done if you have also specified a
|
||||
> `volume_driver`.
|
||||
|
||||
|
|
@ -519,8 +575,18 @@ specifying read-only access(``ro``) or read-write(``rw``).
|
|||
|
||||
volumes_from:
|
||||
- service_name
|
||||
- container_name
|
||||
- service_name:rw
|
||||
- service_name:ro
|
||||
- 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
|
||||
|
||||
|
|
@ -562,20 +628,323 @@ subcommand documentation for more information.
|
|||
### driver
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
driver_opts:
|
||||
foo: "bar"
|
||||
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). They *can*, however, define [links](#links).
|
||||
|
||||
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.
|
||||
|
||||
You cannot define links when using version 2. Instead, you should use
|
||||
[networking](networking.md) for communication between containers. In most cases,
|
||||
this will involve less configuration than links.
|
||||
|
||||
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
|
||||
- back
|
||||
redis:
|
||||
image: redis
|
||||
volumes:
|
||||
- data:/var/lib/redis
|
||||
networks:
|
||||
- back
|
||||
volumes:
|
||||
data:
|
||||
driver: local
|
||||
networks:
|
||||
front:
|
||||
driver: bridge
|
||||
back:
|
||||
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](env.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"
|
||||
|
||||
|
||||
## Variable substitution
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ weight=3
|
|||
|
||||
# Compose 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -45,8 +45,7 @@ A `docker-compose.yml` looks like this:
|
|||
redis:
|
||||
image: redis
|
||||
volumes:
|
||||
logvolume01:
|
||||
driver: default
|
||||
logvolume01: {}
|
||||
|
||||
For more information about the Compose file, see the
|
||||
[Compose file reference](compose-file.md)
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ which the release page specifies, in your terminal.
|
|||
|
||||
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.0rc2/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
|
||||
|
||||
If you have problems installing with `curl`, see
|
||||
[Alternative Install Options](#alternative-install-options).
|
||||
|
|
@ -54,7 +54,7 @@ which the release page specifies, in your terminal.
|
|||
7. Test the installation.
|
||||
|
||||
$ docker-compose --version
|
||||
docker-compose version: 1.5.2
|
||||
docker-compose version: 1.6.0rc2
|
||||
|
||||
|
||||
## Alternative install options
|
||||
|
|
@ -77,7 +77,7 @@ to get started.
|
|||
Compose can also be run inside a container, from a small bash script wrapper.
|
||||
To install compose as a container run:
|
||||
|
||||
$ curl -L https://github.com/docker/compose/releases/download/1.5.2/run.sh > /usr/local/bin/docker-compose
|
||||
$ curl -L https://github.com/docker/compose/releases/download/1.6.0rc2/run.sh > /usr/local/bin/docker-compose
|
||||
$ chmod +x /usr/local/bin/docker-compose
|
||||
|
||||
## Master builds
|
||||
|
|
|
|||
|
|
@ -12,83 +12,135 @@ weight=6
|
|||
|
||||
# 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
|
||||
container for a service joins the default network and is both *reachable* by
|
||||
other containers on that network, and *discoverable* by them at a hostname
|
||||
identical to the container name.
|
||||
|
||||
> **Note:** Your app's network is given 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/docker-compose.md) or the [`COMPOSE_PROJECT_NAME` environment variable](reference/overview.md#compose-project-name).
|
||||
|
||||
For example, suppose your app is in a directory called `myapp`, and your `docker-compose.yml` looks like this:
|
||||
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "8000:8000"
|
||||
db:
|
||||
image: postgres
|
||||
version: 2
|
||||
|
||||
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.
|
||||
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`.
|
||||
When you run `docker-compose up`, the following happens:
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
|
||||
(TODO: talk about Swarm and the overlay driver)
|
||||
<![end-metadata]-->
|
||||
When deploying a Compose application to a Swarm cluster, you can make use of the built-in `overlay` driver to enable multi-host communication between containers with no changes to application code. Consult the [Getting started with multi-host networking](/engine/userguide/networking/get-started-overlay.md) to see how to set up the overlay driver, and then specify `driver: overlay` in your networking config (see the sections below for how to do this).
|
||||
|
||||
## Custom container network modes
|
||||
## Specifying custom networks
|
||||
|
||||
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.
|
||||
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 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.
|
||||
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.
|
||||
|
||||
If *all* services in an app specify the `net` option, a network will not be created at all.
|
||||
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 the overlay driver for multi-host communication
|
||||
driver: overlay
|
||||
back:
|
||||
# Use a custom driver which takes special options
|
||||
driver: my-custom-driver
|
||||
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 the overlay driver for multi-host communication
|
||||
driver: overlay
|
||||
|
||||
## 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.
|
||||
|
|
|
|||
|
|
@ -85,12 +85,12 @@ 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
|
||||
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,
|
||||
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.
|
||||
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](overview.md#compose-file).
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
PyYAML==3.11
|
||||
cached-property==1.2.0
|
||||
dockerpty==0.3.4
|
||||
docker-py==1.7.0rc3
|
||||
docopt==0.6.1
|
||||
enum34==1.0.4
|
||||
git+https://github.com/docker/docker-py.git@master#egg=docker-py
|
||||
git+https://github.com/d11wtq/dockerpty.git@29b1394108b017ef3e3deaf00604a9eb99880d5e#egg=dockerpty
|
||||
jsonschema==2.5.1
|
||||
requests==2.7.0
|
||||
six==1.7.3
|
||||
|
|
|
|||
|
|
@ -41,6 +41,9 @@ Get-ChildItem -Recurse -Include *.pyc | foreach ($_) { Remove-Item $_.FullName }
|
|||
# Create virtualenv
|
||||
virtualenv .\venv
|
||||
|
||||
# pip and pyinstaller generate lots of warnings, so we need to ignore them
|
||||
$ErrorActionPreference = "Continue"
|
||||
|
||||
# Install dependencies
|
||||
.\venv\Scripts\pip install pypiwin32==219
|
||||
.\venv\Scripts\pip install -r requirements.txt
|
||||
|
|
@ -50,8 +53,6 @@ virtualenv .\venv
|
|||
git rev-parse --short HEAD | out-file -encoding ASCII compose\GITSHA
|
||||
|
||||
# Build binary
|
||||
# pyinstaller has lots of warnings, so we need to run with ErrorAction = Continue
|
||||
$ErrorActionPreference = "Continue"
|
||||
.\venv\Scripts\pyinstaller .\docker-compose.spec
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
|
|
|
|||
|
|
@ -18,10 +18,13 @@ PREV_RELEASE=$1
|
|||
VERSION=HEAD
|
||||
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' | \
|
||||
sort | \
|
||||
uniq -c | \
|
||||
sort -nr | \
|
||||
awk '{print "@"$2","}' | \
|
||||
xargs echo
|
||||
sort -nr)
|
||||
|
||||
echo "Contributions by user: "
|
||||
echo "$contribs"
|
||||
echo
|
||||
echo "$contribs" | awk '{print "@"$2","}' | xargs
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ sed -i -e 's/logo.png?raw=true/https:\/\/github.com\/docker\/compose\/raw\/maste
|
|||
./script/write-git-sha
|
||||
python setup.py sdist
|
||||
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
|
||||
python setup.py upload
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
# $Env:DOCKER_COMPOSE_OPTIONS.
|
||||
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
set -e
|
||||
|
||||
VERSION="1.5.2"
|
||||
VERSION="1.6.0rc2"
|
||||
IMAGE="docker/compose:$VERSION"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ get_versions="docker run --rm
|
|||
if [ "$DOCKER_VERSIONS" == "" ]; then
|
||||
DOCKER_VERSIONS="$($get_versions default)"
|
||||
elif [ "$DOCKER_VERSIONS" == "all" ]; then
|
||||
DOCKER_VERSIONS="1.9.1 1.10.0-dev"
|
||||
DOCKER_VERSIONS=$($get_versions -n 2 recent)
|
||||
fi
|
||||
|
||||
|
||||
|
|
@ -38,11 +38,7 @@ for version in $DOCKER_VERSIONS; do
|
|||
|
||||
trap "on_exit" EXIT
|
||||
|
||||
if [[ $version == *"-dev" ]]; then
|
||||
repo="dnephin/dind"
|
||||
else
|
||||
repo="dockerswarm/dind"
|
||||
fi
|
||||
repo="dockerswarm/dind"
|
||||
|
||||
docker run \
|
||||
-d \
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ def wait_on_process(proc, returncode=0):
|
|||
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()
|
||||
while not condition():
|
||||
if time.time() - start_time > timeout:
|
||||
|
|
@ -409,6 +409,7 @@ class CLITestCase(DockerClientTestCase):
|
|||
networks = self.client.networks(names=[self.project.default_network.full_name])
|
||||
self.assertEqual(len(networks), 1)
|
||||
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'])
|
||||
|
||||
|
|
@ -419,12 +420,28 @@ class CLITestCase(DockerClientTestCase):
|
|||
container = containers[0]
|
||||
self.assertIn(container.id, network['Containers'])
|
||||
|
||||
networks = list(container.get('NetworkSettings.Networks'))
|
||||
self.assertEqual(networks, [network['Name']])
|
||||
networks = container.get('NetworkSettings.Networks')
|
||||
self.assertEqual(list(networks), [network['Name']])
|
||||
|
||||
self.assertEqual(
|
||||
sorted(networks[network['Name']]['Aliases']),
|
||||
sorted([service.name, container.short_id]))
|
||||
|
||||
for service in services:
|
||||
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)
|
||||
|
||||
networks = self.client.networks(names=[self.project.default_network.full_name])
|
||||
assert networks[0]['Options']['com.docker.network.bridge.enable_icc'] == 'false'
|
||||
|
||||
@v2_only()
|
||||
def test_up_with_networks(self):
|
||||
self.base_dir = 'tests/fixtures/networks'
|
||||
|
|
@ -448,6 +465,10 @@ class CLITestCase(DockerClientTestCase):
|
|||
app_container = self.project.get_service('app').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
|
||||
assert sorted(back_network['Containers']) == sorted([db_container.id, app_container.id])
|
||||
|
||||
|
|
@ -461,6 +482,9 @@ class CLITestCase(DockerClientTestCase):
|
|||
# app can see db
|
||||
assert self.lookup(app_container, "db")
|
||||
|
||||
# app has aliased db to "database"
|
||||
assert self.lookup(app_container, "database")
|
||||
|
||||
@v2_only()
|
||||
def test_up_missing_network(self):
|
||||
self.base_dir = 'tests/fixtures/networks'
|
||||
|
|
@ -472,8 +496,13 @@ class CLITestCase(DockerClientTestCase):
|
|||
assert 'Service "web" uses an undefined network "foo"' in result.stderr
|
||||
|
||||
@v2_only()
|
||||
def test_up_predefined_networks(self):
|
||||
filename = 'predefined-networks.yml'
|
||||
def test_up_with_network_mode(self):
|
||||
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._project = get_project(self.base_dir, [filename])
|
||||
|
|
@ -491,6 +520,16 @@ class CLITestCase(DockerClientTestCase):
|
|||
assert list(container.get('NetworkSettings.Networks')) == [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()
|
||||
def test_up_external_networks(self):
|
||||
filename = 'external-networks.yml'
|
||||
|
|
@ -515,6 +554,29 @@ class CLITestCase(DockerClientTestCase):
|
|||
container = self.project.containers()[0]
|
||||
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()
|
||||
def test_up_no_services(self):
|
||||
self.base_dir = 'tests/fixtures/no-services'
|
||||
|
|
@ -530,18 +592,6 @@ class CLITestCase(DockerClientTestCase):
|
|||
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):
|
||||
self.base_dir = 'tests/fixtures/links-composefile'
|
||||
self.dispatch(['up', '-d', 'web'], None)
|
||||
|
|
@ -648,14 +698,14 @@ class CLITestCase(DockerClientTestCase):
|
|||
wait_on_condition(ContainerCountCondition(self.project, 2))
|
||||
|
||||
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):
|
||||
proc = start_process(self.base_dir, ['up', '-t', '2'])
|
||||
wait_on_condition(ContainerCountCondition(self.project, 2))
|
||||
|
||||
os.kill(proc.pid, signal.SIGTERM)
|
||||
wait_on_condition(ContainerCountCondition(self.project, 0), timeout=30)
|
||||
wait_on_condition(ContainerCountCondition(self.project, 0))
|
||||
|
||||
def test_run_service_without_links(self):
|
||||
self.base_dir = 'tests/fixtures/links-composefile'
|
||||
|
|
@ -872,14 +922,47 @@ class CLITestCase(DockerClientTestCase):
|
|||
self.assertEqual(container.name, name)
|
||||
|
||||
@v2_only()
|
||||
def test_run_with_networking(self):
|
||||
self.base_dir = 'tests/fixtures/v2-simple'
|
||||
self.dispatch(['run', 'simple', 'true'], None)
|
||||
service = self.project.get_service('simple')
|
||||
container, = service.containers(stopped=True, one_off=True)
|
||||
networks = self.client.networks(names=[self.project.default_network.full_name])
|
||||
self.assertEqual(len(networks), 1)
|
||||
self.assertEqual(container.human_readable_command, u'true')
|
||||
def test_run_interactive_connects_to_network(self):
|
||||
self.base_dir = 'tests/fixtures/networks'
|
||||
|
||||
self.dispatch(['up', '-d'])
|
||||
self.dispatch(['run', 'app', 'nslookup', 'app'])
|
||||
self.dispatch(['run', 'app', 'nslookup', 'db'])
|
||||
|
||||
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):
|
||||
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:
|
||||
image: busybox
|
||||
command: /bin/true
|
||||
net: host
|
||||
environment:
|
||||
- FOO=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"
|
||||
# add BAZ
|
||||
BAZ: "2"
|
||||
net: bridge
|
||||
mydb:
|
||||
image: busybox
|
||||
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: '.'
|
||||
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"
|
||||
2
tests/fixtures/networks/docker-compose.yml
vendored
2
tests/fixtures/networks/docker-compose.yml
vendored
|
|
@ -9,6 +9,8 @@ services:
|
|||
image: busybox
|
||||
command: top
|
||||
networks: ["front", "back"]
|
||||
links:
|
||||
- "db:database"
|
||||
db:
|
||||
image: busybox
|
||||
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
|
||||
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: []
|
||||
|
|
@ -4,10 +4,12 @@ from __future__ import unicode_literals
|
|||
import random
|
||||
|
||||
import py
|
||||
import pytest
|
||||
from docker.errors import NotFound
|
||||
|
||||
from .testcases import DockerClientTestCase
|
||||
from compose.config import config
|
||||
from compose.config import ConfigurationError
|
||||
from compose.config.types import VolumeFromSpec
|
||||
from compose.config.types import VolumeSpec
|
||||
from compose.const import LABEL_PROJECT
|
||||
|
|
@ -104,7 +106,71 @@ class ProjectTest(DockerClientTestCase):
|
|||
db = project.get_service('db')
|
||||
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': 2,
|
||||
'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': 2,
|
||||
'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(
|
||||
name='composetest',
|
||||
config_data=build_service_dicts({
|
||||
|
|
@ -125,9 +191,26 @@ class ProjectTest(DockerClientTestCase):
|
|||
|
||||
web = project.get_service('web')
|
||||
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(
|
||||
self.client,
|
||||
image='busybox:latest',
|
||||
|
|
@ -137,21 +220,11 @@ class ProjectTest(DockerClientTestCase):
|
|||
)
|
||||
net_container.start()
|
||||
|
||||
project = Project.from_config(
|
||||
name='composetest',
|
||||
config_data=build_service_dicts({
|
||||
'web': {
|
||||
'image': 'busybox:latest',
|
||||
'net': 'container:composetest_net_container'
|
||||
},
|
||||
}),
|
||||
client=self.client,
|
||||
)
|
||||
|
||||
project = get_project()
|
||||
project.up()
|
||||
|
||||
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):
|
||||
web = self.create_service('web')
|
||||
|
|
@ -516,6 +589,63 @@ class ProjectTest(DockerClientTestCase):
|
|||
foo_data = self.client.inspect_network('composetest_foo')
|
||||
self.assertEqual(foo_data['Driver'], 'bridge')
|
||||
|
||||
@v2_only()
|
||||
def test_up_with_ipam_config(self):
|
||||
config_data = config.Config(
|
||||
version=2,
|
||||
services=[],
|
||||
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()
|
||||
def test_project_up_volumes(self):
|
||||
vol_name = '{0:x}'.format(random.getrandbits(32))
|
||||
|
|
@ -756,3 +886,40 @@ class ProjectTest(DockerClientTestCase):
|
|||
assert 'Volume {0} declared as external'.format(
|
||||
vol_name
|
||||
) 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': 2,
|
||||
'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 docker.errors import APIError
|
||||
from pytest import mark
|
||||
from six import StringIO
|
||||
from six import text_type
|
||||
|
||||
|
|
@ -27,7 +26,7 @@ from compose.const import LABEL_VERSION
|
|||
from compose.container import Container
|
||||
from compose.service import ConvergencePlan
|
||||
from compose.service import ConvergenceStrategy
|
||||
from compose.service import Net
|
||||
from compose.service import NetworkMode
|
||||
from compose.service import Service
|
||||
|
||||
|
||||
|
|
@ -372,7 +371,6 @@ class ServiceTest(DockerClientTestCase):
|
|||
create_and_start_container(db)
|
||||
self.assertEqual(db.containers()[0].environment['FOO'], 'BAR')
|
||||
|
||||
@mark.skipif(True, reason="Engine returns error - needs investigating")
|
||||
def test_start_container_creates_links(self):
|
||||
db = self.create_service('db')
|
||||
web = self.create_service('web', links=[(db, None)])
|
||||
|
|
@ -389,7 +387,6 @@ class ServiceTest(DockerClientTestCase):
|
|||
'db'])
|
||||
)
|
||||
|
||||
@mark.skipif(True, reason="Engine returns error - needs investigating")
|
||||
def test_start_container_creates_links_with_names(self):
|
||||
db = self.create_service('db')
|
||||
web = self.create_service('web', links=[(db, 'custom_link_name')])
|
||||
|
|
@ -433,7 +430,6 @@ class ServiceTest(DockerClientTestCase):
|
|||
c = create_and_start_container(db)
|
||||
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):
|
||||
db = self.create_service('db')
|
||||
|
||||
|
|
@ -750,18 +746,23 @@ class ServiceTest(DockerClientTestCase):
|
|||
for container in containers:
|
||||
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):
|
||||
service = self.create_service('web', net=Net('none'))
|
||||
service = self.create_service('web', network_mode=NetworkMode('none'))
|
||||
container = create_and_start_container(service)
|
||||
self.assertEqual(container.get('HostConfig.NetworkMode'), 'none')
|
||||
|
||||
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)
|
||||
self.assertEqual(container.get('HostConfig.NetworkMode'), 'bridge')
|
||||
|
||||
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)
|
||||
self.assertEqual(container.get('HostConfig.NetworkMode'), 'host')
|
||||
|
||||
|
|
|
|||
|
|
@ -72,8 +72,39 @@ class CLITestCase(unittest.TestCase):
|
|||
TopLevelCommand().dispatch(['help', 'nonexistent'], None)
|
||||
|
||||
@pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason="requires dockerpty")
|
||||
@mock.patch('compose.cli.main.dockerpty', 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):
|
||||
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_pseudo_terminal.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()
|
||||
mock_client = mock.create_autospec(docker.Client)
|
||||
mock_project = mock.Mock(client=mock_client)
|
||||
|
|
|
|||
|
|
@ -25,14 +25,15 @@ V1 = 1
|
|||
|
||||
|
||||
def make_service_dict(name, service_dict, working_dir, filename=None):
|
||||
"""Test helper function to construct a ServiceExtendsResolver
|
||||
"""
|
||||
Test helper function to construct a ServiceExtendsResolver
|
||||
"""
|
||||
resolver = config.ServiceExtendsResolver(config.ServiceConfig(
|
||||
working_dir=working_dir,
|
||||
filename=filename,
|
||||
name=name,
|
||||
config=service_dict), version=1)
|
||||
resolver = config.ServiceExtendsResolver(
|
||||
config.ServiceConfig(
|
||||
working_dir=working_dir,
|
||||
filename=filename,
|
||||
name=name,
|
||||
config=service_dict),
|
||||
config.ConfigFile(filename=filename, config={}))
|
||||
return config.process_service(resolver.run())
|
||||
|
||||
|
||||
|
|
@ -87,11 +88,26 @@ class ConfigTest(unittest.TestCase):
|
|||
'driver': 'default',
|
||||
'driver_opts': {'beep': 'boop'}
|
||||
}
|
||||
},
|
||||
'networks': {
|
||||
'default': {
|
||||
'driver': 'bridge',
|
||||
'driver_opts': {'beep': 'boop'}
|
||||
},
|
||||
'with_ipam': {
|
||||
'ipam': {
|
||||
'driver': 'default',
|
||||
'config': [
|
||||
{'subnet': '172.28.0.0/16'}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 'working_dir', 'filename.yml')
|
||||
)
|
||||
service_dicts = config_data.services
|
||||
volume_dict = config_data.volumes
|
||||
networks_dict = config_data.networks
|
||||
self.assertEqual(
|
||||
service_sort(service_dicts),
|
||||
service_sort([
|
||||
|
|
@ -112,6 +128,20 @@ class ConfigTest(unittest.TestCase):
|
|||
'driver_opts': {'beep': 'boop'}
|
||||
}
|
||||
})
|
||||
self.assertEqual(networks_dict, {
|
||||
'default': {
|
||||
'driver': 'bridge',
|
||||
'driver_opts': {'beep': 'boop'}
|
||||
},
|
||||
'with_ipam': {
|
||||
'ipam': {
|
||||
'driver': 'default',
|
||||
'config': [
|
||||
{'subnet': '172.28.0.0/16'}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
def test_named_volume_config_empty(self):
|
||||
config_details = build_config_details({
|
||||
|
|
@ -190,6 +220,18 @@ class ConfigTest(unittest.TestCase):
|
|||
)
|
||||
)
|
||||
|
||||
def test_load_throws_error_with_invalid_network_fields(self):
|
||||
with self.assertRaises(ConfigurationError):
|
||||
config.load(
|
||||
build_config_details({
|
||||
'version': 2,
|
||||
'services': {'web': 'busybox:latest'},
|
||||
'networks': {
|
||||
'invalid': {'foo', 'bar'}
|
||||
}
|
||||
}, 'working_dir', 'filename.yml')
|
||||
)
|
||||
|
||||
def test_load_config_invalid_service_names(self):
|
||||
for invalid_name in ['?not?allowed', ' ', '', '!', '/', '\xe2']:
|
||||
with pytest.raises(ConfigurationError) as exc:
|
||||
|
|
@ -522,6 +564,56 @@ class ConfigTest(unittest.TestCase):
|
|||
]
|
||||
assert service_sort(service_dicts) == service_sort(expected)
|
||||
|
||||
def test_undeclared_volume_v2(self):
|
||||
base_file = config.ConfigFile(
|
||||
'base.yaml',
|
||||
{
|
||||
'version': 2,
|
||||
'services': {
|
||||
'web': {
|
||||
'image': 'busybox:latest',
|
||||
'volumes': ['data0028:/data:ro'],
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
details = config.ConfigDetails('.', [base_file])
|
||||
with self.assertRaises(ConfigurationError):
|
||||
config.load(details)
|
||||
|
||||
base_file = config.ConfigFile(
|
||||
'base.yaml',
|
||||
{
|
||||
'version': 2,
|
||||
'services': {
|
||||
'web': {
|
||||
'image': 'busybox:latest',
|
||||
'volumes': ['./data0028:/data:ro'],
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
details = config.ConfigDetails('.', [base_file])
|
||||
config_data = config.load(details)
|
||||
volume = config_data.services[0].get('volumes')[0]
|
||||
assert not volume.is_named_volume
|
||||
|
||||
def test_undeclared_volume_v1(self):
|
||||
base_file = config.ConfigFile(
|
||||
'base.yaml',
|
||||
{
|
||||
'web': {
|
||||
'image': 'busybox:latest',
|
||||
'volumes': ['data0028:/data:ro'],
|
||||
},
|
||||
}
|
||||
)
|
||||
details = config.ConfigDetails('.', [base_file])
|
||||
config_data = config.load(details)
|
||||
volume = config_data.services[0].get('volumes')[0]
|
||||
assert volume.external == 'data0028'
|
||||
assert volume.is_named_volume
|
||||
|
||||
def test_config_valid_service_names(self):
|
||||
for valid_name in ['_', '-', '.__.', '_what-up.', 'what_.up----', 'whatup']:
|
||||
services = config.load(
|
||||
|
|
@ -699,7 +791,7 @@ class ConfigTest(unittest.TestCase):
|
|||
assert "'hard' is a required property" in exc.exconly()
|
||||
|
||||
def test_config_ulimits_soft_greater_than_hard_error(self):
|
||||
expected = "cannot contain a 'soft' value higher than 'hard' value"
|
||||
expected = "'soft' value can not be greater than 'hard' value"
|
||||
|
||||
with pytest.raises(ConfigurationError) as exc:
|
||||
config.load(build_config_details(
|
||||
|
|
@ -893,9 +985,155 @@ class ConfigTest(unittest.TestCase):
|
|||
'ext': {'external': True, 'driver': 'foo'}
|
||||
}
|
||||
})
|
||||
with self.assertRaises(ConfigurationError):
|
||||
with pytest.raises(ConfigurationError):
|
||||
config.load(config_details)
|
||||
|
||||
def test_depends_on_orders_services(self):
|
||||
config_details = build_config_details({
|
||||
'version': 2,
|
||||
'services': {
|
||||
'one': {'image': 'busybox', 'depends_on': ['three', 'two']},
|
||||
'two': {'image': 'busybox', 'depends_on': ['three']},
|
||||
'three': {'image': 'busybox'},
|
||||
},
|
||||
})
|
||||
actual = config.load(config_details)
|
||||
assert (
|
||||
[service['name'] for service in actual.services] ==
|
||||
['three', 'two', 'one']
|
||||
)
|
||||
|
||||
def test_depends_on_unknown_service_errors(self):
|
||||
config_details = build_config_details({
|
||||
'version': 2,
|
||||
'services': {
|
||||
'one': {'image': 'busybox', 'depends_on': ['three']},
|
||||
},
|
||||
})
|
||||
with pytest.raises(ConfigurationError) as exc:
|
||||
config.load(config_details)
|
||||
assert "Service 'one' depends on service 'three'" in exc.exconly()
|
||||
|
||||
|
||||
class NetworkModeTest(unittest.TestCase):
|
||||
def test_network_mode_standard(self):
|
||||
config_data = config.load(build_config_details({
|
||||
'version': 2,
|
||||
'services': {
|
||||
'web': {
|
||||
'image': 'busybox',
|
||||
'command': "top",
|
||||
'network_mode': 'bridge',
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
assert config_data.services[0]['network_mode'] == 'bridge'
|
||||
|
||||
def test_network_mode_standard_v1(self):
|
||||
config_data = config.load(build_config_details({
|
||||
'web': {
|
||||
'image': 'busybox',
|
||||
'command': "top",
|
||||
'net': 'bridge',
|
||||
},
|
||||
}))
|
||||
|
||||
assert config_data.services[0]['network_mode'] == 'bridge'
|
||||
assert 'net' not in config_data.services[0]
|
||||
|
||||
def test_network_mode_container(self):
|
||||
config_data = config.load(build_config_details({
|
||||
'version': 2,
|
||||
'services': {
|
||||
'web': {
|
||||
'image': 'busybox',
|
||||
'command': "top",
|
||||
'network_mode': 'container:foo',
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
assert config_data.services[0]['network_mode'] == 'container:foo'
|
||||
|
||||
def test_network_mode_container_v1(self):
|
||||
config_data = config.load(build_config_details({
|
||||
'web': {
|
||||
'image': 'busybox',
|
||||
'command': "top",
|
||||
'net': 'container:foo',
|
||||
},
|
||||
}))
|
||||
|
||||
assert config_data.services[0]['network_mode'] == 'container:foo'
|
||||
|
||||
def test_network_mode_service(self):
|
||||
config_data = config.load(build_config_details({
|
||||
'version': 2,
|
||||
'services': {
|
||||
'web': {
|
||||
'image': 'busybox',
|
||||
'command': "top",
|
||||
'network_mode': 'service:foo',
|
||||
},
|
||||
'foo': {
|
||||
'image': 'busybox',
|
||||
'command': "top",
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
assert config_data.services[1]['network_mode'] == 'service:foo'
|
||||
|
||||
def test_network_mode_service_v1(self):
|
||||
config_data = config.load(build_config_details({
|
||||
'web': {
|
||||
'image': 'busybox',
|
||||
'command': "top",
|
||||
'net': 'container:foo',
|
||||
},
|
||||
'foo': {
|
||||
'image': 'busybox',
|
||||
'command': "top",
|
||||
},
|
||||
}))
|
||||
|
||||
assert config_data.services[1]['network_mode'] == 'service:foo'
|
||||
|
||||
def test_network_mode_service_nonexistent(self):
|
||||
with pytest.raises(ConfigurationError) as excinfo:
|
||||
config.load(build_config_details({
|
||||
'version': 2,
|
||||
'services': {
|
||||
'web': {
|
||||
'image': 'busybox',
|
||||
'command': "top",
|
||||
'network_mode': 'service:foo',
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
assert "service 'foo' which is undefined" in excinfo.exconly()
|
||||
|
||||
def test_network_mode_plus_networks_is_invalid(self):
|
||||
with pytest.raises(ConfigurationError) as excinfo:
|
||||
config.load(build_config_details({
|
||||
'version': 2,
|
||||
'services': {
|
||||
'web': {
|
||||
'image': 'busybox',
|
||||
'command': "top",
|
||||
'network_mode': 'bridge',
|
||||
'networks': ['front'],
|
||||
},
|
||||
},
|
||||
'networks': {
|
||||
'front': None,
|
||||
}
|
||||
}))
|
||||
|
||||
assert "'network_mode' and 'networks' cannot be combined" in excinfo.exconly()
|
||||
|
||||
|
||||
class PortsTest(unittest.TestCase):
|
||||
INVALID_PORTS_TYPES = [
|
||||
|
|
@ -1524,6 +1762,7 @@ class ExtendsTest(unittest.TestCase):
|
|||
'name': 'myweb',
|
||||
'image': 'busybox',
|
||||
'command': 'top',
|
||||
'network_mode': 'bridge',
|
||||
'links': ['mydb:db'],
|
||||
'environment': {
|
||||
"FOO": "1",
|
||||
|
|
@ -1541,6 +1780,7 @@ class ExtendsTest(unittest.TestCase):
|
|||
'name': 'web',
|
||||
'image': 'busybox',
|
||||
'command': '/bin/true',
|
||||
'network_mode': 'host',
|
||||
'environment': {
|
||||
"FOO": "2",
|
||||
"BAR": "1",
|
||||
|
|
@ -1559,6 +1799,7 @@ class ExtendsTest(unittest.TestCase):
|
|||
'name': 'myweb',
|
||||
'image': 'busybox',
|
||||
'command': '/bin/true',
|
||||
'network_mode': 'host',
|
||||
'environment': {
|
||||
"FOO": "2",
|
||||
"BAR": "2",
|
||||
|
|
@ -1749,11 +1990,18 @@ class ExtendsTest(unittest.TestCase):
|
|||
load_from_filename('tests/fixtures/extends/invalid-volumes.yml')
|
||||
|
||||
def test_invalid_net_in_extended_service(self):
|
||||
expected_error_msg = "services with 'net: container' cannot be extended"
|
||||
with pytest.raises(ConfigurationError) as excinfo:
|
||||
load_from_filename('tests/fixtures/extends/invalid-net-v2.yml')
|
||||
|
||||
with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
|
||||
assert 'network_mode: service' in excinfo.exconly()
|
||||
assert 'cannot be extended' in excinfo.exconly()
|
||||
|
||||
with pytest.raises(ConfigurationError) as excinfo:
|
||||
load_from_filename('tests/fixtures/extends/invalid-net.yml')
|
||||
|
||||
assert 'net: container' in excinfo.exconly()
|
||||
assert 'cannot be extended' in excinfo.exconly()
|
||||
|
||||
@mock.patch.dict(os.environ)
|
||||
def test_load_config_runs_interpolation_in_extended_service(self):
|
||||
os.environ.update(HOSTNAME_VALUE="penguin")
|
||||
|
|
@ -1888,6 +2136,52 @@ class ExtendsTest(unittest.TestCase):
|
|||
|
||||
assert config == expected
|
||||
|
||||
def test_extends_with_mixed_versions_is_error(self):
|
||||
tmpdir = py.test.ensuretemp('test_extends_with_mixed_version')
|
||||
self.addCleanup(tmpdir.remove)
|
||||
tmpdir.join('docker-compose.yml').write("""
|
||||
version: 2
|
||||
services:
|
||||
web:
|
||||
extends:
|
||||
file: base.yml
|
||||
service: base
|
||||
image: busybox
|
||||
""")
|
||||
tmpdir.join('base.yml').write("""
|
||||
base:
|
||||
volumes: ['/foo']
|
||||
ports: ['3000:3000']
|
||||
""")
|
||||
|
||||
with pytest.raises(ConfigurationError) as exc:
|
||||
load_from_filename(str(tmpdir.join('docker-compose.yml')))
|
||||
assert 'Version mismatch' in exc.exconly()
|
||||
|
||||
def test_extends_with_defined_version_passes(self):
|
||||
tmpdir = py.test.ensuretemp('test_extends_with_defined_version')
|
||||
self.addCleanup(tmpdir.remove)
|
||||
tmpdir.join('docker-compose.yml').write("""
|
||||
version: 2
|
||||
services:
|
||||
web:
|
||||
extends:
|
||||
file: base.yml
|
||||
service: base
|
||||
image: busybox
|
||||
""")
|
||||
tmpdir.join('base.yml').write("""
|
||||
version: 2
|
||||
services:
|
||||
base:
|
||||
volumes: ['/foo']
|
||||
ports: ['3000:3000']
|
||||
command: top
|
||||
""")
|
||||
|
||||
service = load_from_filename(str(tmpdir.join('docker-compose.yml')))
|
||||
self.assertEquals(service[0]['command'], "top")
|
||||
|
||||
|
||||
@pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash')
|
||||
class ExpandPathTest(unittest.TestCase):
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pytest
|
||||
|
||||
from compose.config.errors import DependencyError
|
||||
from compose.config.sort_services import sort_service_dicts
|
||||
from compose.config.types import VolumeFromSpec
|
||||
from tests import unittest
|
||||
|
||||
|
||||
class SortServiceTest(unittest.TestCase):
|
||||
class TestSortService(object):
|
||||
def test_sort_service_dicts_1(self):
|
||||
services = [
|
||||
{
|
||||
|
|
@ -23,10 +24,10 @@ class SortServiceTest(unittest.TestCase):
|
|||
]
|
||||
|
||||
sorted_services = sort_service_dicts(services)
|
||||
self.assertEqual(len(sorted_services), 3)
|
||||
self.assertEqual(sorted_services[0]['name'], 'grunt')
|
||||
self.assertEqual(sorted_services[1]['name'], 'redis')
|
||||
self.assertEqual(sorted_services[2]['name'], 'web')
|
||||
assert len(sorted_services) == 3
|
||||
assert sorted_services[0]['name'] == 'grunt'
|
||||
assert sorted_services[1]['name'] == 'redis'
|
||||
assert sorted_services[2]['name'] == 'web'
|
||||
|
||||
def test_sort_service_dicts_2(self):
|
||||
services = [
|
||||
|
|
@ -44,10 +45,10 @@ class SortServiceTest(unittest.TestCase):
|
|||
]
|
||||
|
||||
sorted_services = sort_service_dicts(services)
|
||||
self.assertEqual(len(sorted_services), 3)
|
||||
self.assertEqual(sorted_services[0]['name'], 'redis')
|
||||
self.assertEqual(sorted_services[1]['name'], 'postgres')
|
||||
self.assertEqual(sorted_services[2]['name'], 'web')
|
||||
assert len(sorted_services) == 3
|
||||
assert sorted_services[0]['name'] == 'redis'
|
||||
assert sorted_services[1]['name'] == 'postgres'
|
||||
assert sorted_services[2]['name'] == 'web'
|
||||
|
||||
def test_sort_service_dicts_3(self):
|
||||
services = [
|
||||
|
|
@ -65,10 +66,10 @@ class SortServiceTest(unittest.TestCase):
|
|||
]
|
||||
|
||||
sorted_services = sort_service_dicts(services)
|
||||
self.assertEqual(len(sorted_services), 3)
|
||||
self.assertEqual(sorted_services[0]['name'], 'child')
|
||||
self.assertEqual(sorted_services[1]['name'], 'parent')
|
||||
self.assertEqual(sorted_services[2]['name'], 'grandparent')
|
||||
assert len(sorted_services) == 3
|
||||
assert sorted_services[0]['name'] == 'child'
|
||||
assert sorted_services[1]['name'] == 'parent'
|
||||
assert sorted_services[2]['name'] == 'grandparent'
|
||||
|
||||
def test_sort_service_dicts_4(self):
|
||||
services = [
|
||||
|
|
@ -86,10 +87,10 @@ class SortServiceTest(unittest.TestCase):
|
|||
]
|
||||
|
||||
sorted_services = sort_service_dicts(services)
|
||||
self.assertEqual(len(sorted_services), 3)
|
||||
self.assertEqual(sorted_services[0]['name'], 'child')
|
||||
self.assertEqual(sorted_services[1]['name'], 'parent')
|
||||
self.assertEqual(sorted_services[2]['name'], 'grandparent')
|
||||
assert len(sorted_services) == 3
|
||||
assert sorted_services[0]['name'] == 'child'
|
||||
assert sorted_services[1]['name'] == 'parent'
|
||||
assert sorted_services[2]['name'] == 'grandparent'
|
||||
|
||||
def test_sort_service_dicts_5(self):
|
||||
services = [
|
||||
|
|
@ -99,7 +100,7 @@ class SortServiceTest(unittest.TestCase):
|
|||
},
|
||||
{
|
||||
'name': 'parent',
|
||||
'net': 'container:child'
|
||||
'network_mode': 'service:child'
|
||||
},
|
||||
{
|
||||
'name': 'child'
|
||||
|
|
@ -107,10 +108,10 @@ class SortServiceTest(unittest.TestCase):
|
|||
]
|
||||
|
||||
sorted_services = sort_service_dicts(services)
|
||||
self.assertEqual(len(sorted_services), 3)
|
||||
self.assertEqual(sorted_services[0]['name'], 'child')
|
||||
self.assertEqual(sorted_services[1]['name'], 'parent')
|
||||
self.assertEqual(sorted_services[2]['name'], 'grandparent')
|
||||
assert len(sorted_services) == 3
|
||||
assert sorted_services[0]['name'] == 'child'
|
||||
assert sorted_services[1]['name'] == 'parent'
|
||||
assert sorted_services[2]['name'] == 'grandparent'
|
||||
|
||||
def test_sort_service_dicts_6(self):
|
||||
services = [
|
||||
|
|
@ -128,15 +129,15 @@ class SortServiceTest(unittest.TestCase):
|
|||
]
|
||||
|
||||
sorted_services = sort_service_dicts(services)
|
||||
self.assertEqual(len(sorted_services), 3)
|
||||
self.assertEqual(sorted_services[0]['name'], 'child')
|
||||
self.assertEqual(sorted_services[1]['name'], 'parent')
|
||||
self.assertEqual(sorted_services[2]['name'], 'grandparent')
|
||||
assert len(sorted_services) == 3
|
||||
assert sorted_services[0]['name'] == 'child'
|
||||
assert sorted_services[1]['name'] == 'parent'
|
||||
assert sorted_services[2]['name'] == 'grandparent'
|
||||
|
||||
def test_sort_service_dicts_7(self):
|
||||
services = [
|
||||
{
|
||||
'net': 'container:three',
|
||||
'network_mode': 'service:three',
|
||||
'name': 'four'
|
||||
},
|
||||
{
|
||||
|
|
@ -153,11 +154,11 @@ class SortServiceTest(unittest.TestCase):
|
|||
]
|
||||
|
||||
sorted_services = sort_service_dicts(services)
|
||||
self.assertEqual(len(sorted_services), 4)
|
||||
self.assertEqual(sorted_services[0]['name'], 'one')
|
||||
self.assertEqual(sorted_services[1]['name'], 'two')
|
||||
self.assertEqual(sorted_services[2]['name'], 'three')
|
||||
self.assertEqual(sorted_services[3]['name'], 'four')
|
||||
assert len(sorted_services) == 4
|
||||
assert sorted_services[0]['name'] == 'one'
|
||||
assert sorted_services[1]['name'] == 'two'
|
||||
assert sorted_services[2]['name'] == 'three'
|
||||
assert sorted_services[3]['name'] == 'four'
|
||||
|
||||
def test_sort_service_dicts_circular_imports(self):
|
||||
services = [
|
||||
|
|
@ -171,13 +172,10 @@ class SortServiceTest(unittest.TestCase):
|
|||
},
|
||||
]
|
||||
|
||||
try:
|
||||
with pytest.raises(DependencyError) as exc:
|
||||
sort_service_dicts(services)
|
||||
except DependencyError as e:
|
||||
self.assertIn('redis', e.msg)
|
||||
self.assertIn('web', e.msg)
|
||||
else:
|
||||
self.fail('Should have thrown an DependencyError')
|
||||
assert 'redis' in exc.exconly()
|
||||
assert 'web' in exc.exconly()
|
||||
|
||||
def test_sort_service_dicts_circular_imports_2(self):
|
||||
services = [
|
||||
|
|
@ -194,13 +192,10 @@ class SortServiceTest(unittest.TestCase):
|
|||
}
|
||||
]
|
||||
|
||||
try:
|
||||
with pytest.raises(DependencyError) as exc:
|
||||
sort_service_dicts(services)
|
||||
except DependencyError as e:
|
||||
self.assertIn('redis', e.msg)
|
||||
self.assertIn('web', e.msg)
|
||||
else:
|
||||
self.fail('Should have thrown an DependencyError')
|
||||
assert 'redis' in exc.exconly()
|
||||
assert 'web' in exc.exconly()
|
||||
|
||||
def test_sort_service_dicts_circular_imports_3(self):
|
||||
services = [
|
||||
|
|
@ -218,13 +213,10 @@ class SortServiceTest(unittest.TestCase):
|
|||
}
|
||||
]
|
||||
|
||||
try:
|
||||
with pytest.raises(DependencyError) as exc:
|
||||
sort_service_dicts(services)
|
||||
except DependencyError as e:
|
||||
self.assertIn('a', e.msg)
|
||||
self.assertIn('b', e.msg)
|
||||
else:
|
||||
self.fail('Should have thrown an DependencyError')
|
||||
assert 'a' in exc.exconly()
|
||||
assert 'b' in exc.exconly()
|
||||
|
||||
def test_sort_service_dicts_self_imports(self):
|
||||
services = [
|
||||
|
|
@ -234,9 +226,18 @@ class SortServiceTest(unittest.TestCase):
|
|||
},
|
||||
]
|
||||
|
||||
try:
|
||||
with pytest.raises(DependencyError) as exc:
|
||||
sort_service_dicts(services)
|
||||
except DependencyError as e:
|
||||
self.assertIn('web', e.msg)
|
||||
else:
|
||||
self.fail('Should have thrown an DependencyError')
|
||||
assert 'web' in exc.exconly()
|
||||
|
||||
def test_sort_service_dicts_depends_on_self(self):
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -349,7 +349,7 @@ class ProjectTest(unittest.TestCase):
|
|||
),
|
||||
)
|
||||
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({}))
|
||||
|
||||
def test_use_net_from_container(self):
|
||||
|
|
@ -365,7 +365,7 @@ class ProjectTest(unittest.TestCase):
|
|||
{
|
||||
'name': 'test',
|
||||
'image': 'busybox:latest',
|
||||
'net': 'container:aaa'
|
||||
'network_mode': 'container:aaa'
|
||||
},
|
||||
],
|
||||
networks=None,
|
||||
|
|
@ -373,7 +373,7 @@ class ProjectTest(unittest.TestCase):
|
|||
),
|
||||
)
|
||||
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):
|
||||
container_name = 'test_aaa_1'
|
||||
|
|
@ -398,7 +398,7 @@ class ProjectTest(unittest.TestCase):
|
|||
{
|
||||
'name': 'test',
|
||||
'image': 'busybox:latest',
|
||||
'net': 'container:aaa'
|
||||
'network_mode': 'service:aaa'
|
||||
},
|
||||
],
|
||||
networks=None,
|
||||
|
|
@ -407,7 +407,7 @@ class ProjectTest(unittest.TestCase):
|
|||
)
|
||||
|
||||
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):
|
||||
project = Project.from_config(
|
||||
|
|
|
|||
|
|
@ -15,16 +15,16 @@ from compose.const import LABEL_SERVICE
|
|||
from compose.container import Container
|
||||
from compose.service import build_ulimits
|
||||
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 ImageType
|
||||
from compose.service import merge_volume_bindings
|
||||
from compose.service import NeedsBuildError
|
||||
from compose.service import Net
|
||||
from compose.service import NetworkMode
|
||||
from compose.service import NoSuchImageError
|
||||
from compose.service import parse_repository_tag
|
||||
from compose.service import Service
|
||||
from compose.service import ServiceNet
|
||||
from compose.service import ServiceNetworkMode
|
||||
from compose.service import warn_on_masked_volume
|
||||
|
||||
|
||||
|
|
@ -407,7 +407,7 @@ class ServiceTest(unittest.TestCase):
|
|||
'foo',
|
||||
image='example.com/foo',
|
||||
client=self.mock_client,
|
||||
net=ServiceNet(Service('other')),
|
||||
network_mode=ServiceNetworkMode(Service('other')),
|
||||
links=[(Service('one'), 'one')],
|
||||
volumes_from=[VolumeFromSpec(Service('two'), 'rw', 'service')])
|
||||
|
||||
|
|
@ -421,7 +421,7 @@ class ServiceTest(unittest.TestCase):
|
|||
}
|
||||
self.assertEqual(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'}
|
||||
container = Container(
|
||||
self.mock_client,
|
||||
|
|
@ -430,7 +430,7 @@ class ServiceTest(unittest.TestCase):
|
|||
'foo',
|
||||
image='example.com/foo',
|
||||
client=self.mock_client,
|
||||
net=container)
|
||||
network_mode=ContainerNetworkMode(container))
|
||||
|
||||
config_dict = service.config_dict()
|
||||
expected = {
|
||||
|
|
@ -536,14 +536,6 @@ class ServiceTest(unittest.TestCase):
|
|||
ports=["127.0.0.1:1000-2000:2000-3000"])
|
||||
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):
|
||||
image_name = 'example/web:latest'
|
||||
service = Service('foo', image=image_name)
|
||||
|
|
@ -597,20 +589,20 @@ class BuildUlimitsTestCase(unittest.TestCase):
|
|||
|
||||
class NetTestCase(unittest.TestCase):
|
||||
|
||||
def test_net(self):
|
||||
net = Net('host')
|
||||
self.assertEqual(net.id, 'host')
|
||||
self.assertEqual(net.mode, 'host')
|
||||
self.assertEqual(net.service_name, None)
|
||||
def test_network_mode(self):
|
||||
network_mode = NetworkMode('host')
|
||||
self.assertEqual(network_mode.id, 'host')
|
||||
self.assertEqual(network_mode.mode, 'host')
|
||||
self.assertEqual(network_mode.service_name, None)
|
||||
|
||||
def test_net_container(self):
|
||||
def test_network_mode_container(self):
|
||||
container_id = 'abcd'
|
||||
net = ContainerNet(Container(None, {'Id': container_id}))
|
||||
self.assertEqual(net.id, container_id)
|
||||
self.assertEqual(net.mode, 'container:' + container_id)
|
||||
self.assertEqual(net.service_name, None)
|
||||
network_mode = ContainerNetworkMode(Container(None, {'Id': container_id}))
|
||||
self.assertEqual(network_mode.id, container_id)
|
||||
self.assertEqual(network_mode.mode, 'container:' + container_id)
|
||||
self.assertEqual(network_mode.service_name, None)
|
||||
|
||||
def test_net_service(self):
|
||||
def test_network_mode_service(self):
|
||||
container_id = 'bbbb'
|
||||
service_name = 'web'
|
||||
mock_client = mock.create_autospec(docker.Client)
|
||||
|
|
@ -619,23 +611,23 @@ class NetTestCase(unittest.TestCase):
|
|||
]
|
||||
|
||||
service = Service(name=service_name, client=mock_client)
|
||||
net = ServiceNet(service)
|
||||
network_mode = ServiceNetworkMode(service)
|
||||
|
||||
self.assertEqual(net.id, service_name)
|
||||
self.assertEqual(net.mode, 'container:' + container_id)
|
||||
self.assertEqual(net.service_name, service_name)
|
||||
self.assertEqual(network_mode.id, service_name)
|
||||
self.assertEqual(network_mode.mode, 'container:' + container_id)
|
||||
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'
|
||||
mock_client = mock.create_autospec(docker.Client)
|
||||
mock_client.containers.return_value = []
|
||||
|
||||
service = Service(name=service_name, client=mock_client)
|
||||
net = ServiceNet(service)
|
||||
network_mode = ServiceNetworkMode(service)
|
||||
|
||||
self.assertEqual(net.id, service_name)
|
||||
self.assertEqual(net.mode, None)
|
||||
self.assertEqual(net.service_name, service_name)
|
||||
self.assertEqual(network_mode.id, service_name)
|
||||
self.assertEqual(network_mode.mode, None)
|
||||
self.assertEqual(network_mode.service_name, service_name)
|
||||
|
||||
|
||||
def build_mount(destination, source, mode='rw'):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue