diff --git a/CHANGES.md b/CHANGES.md index 88e725da6..ad29512aa 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,22 @@ Change log ========== +1.4.1 (2015-09-10) +------------------ + +The following bugs have been fixed: + +- Some configuration changes (notably changes to `links`, `volumes_from`, and + `net`) were not properly triggering a container recreate as part of + `docker-compose up`. +- `docker-compose up ` was showing logs for all services instead of + just the specified services. +- Containers with custom container names were showing up in logs as + `service_number` instead of their custom container name. +- When scaling a service sometimes containers would be recreated even when + the configuration had not changed. + + 1.4.0 (2015-08-04) ------------------ diff --git a/compose/__init__.py b/compose/__init__.py index 04a072d5a..8d684354d 100644 --- a/compose/__init__.py +++ b/compose/__init__.py @@ -1,3 +1,3 @@ from __future__ import unicode_literals -__version__ = '1.4.0' +__version__ = '1.4.1' diff --git a/compose/cli/main.py b/compose/cli/main.py index 56f6c0505..d9bda2937 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -304,7 +304,7 @@ class TopLevelCommand(Command): log.warn(INSECURE_SSL_WARNING) if not options['--no-deps']: - deps = service.get_linked_names() + deps = service.get_linked_service_names() if len(deps) > 0: project.up( @@ -496,19 +496,8 @@ class TopLevelCommand(Command): ) if not detached: - print("Attaching to", list_containers(to_attach)) - log_printer = LogPrinter(to_attach, attach_params={"logs": True}, monochrome=monochrome) - - try: - log_printer.run() - finally: - def handler(signal, frame): - project.kill(service_names=service_names) - sys.exit(0) - signal.signal(signal.SIGINT, handler) - - print("Gracefully stopping... (press Ctrl+C again to force)") - project.stop(service_names=service_names, timeout=timeout) + log_printer = build_log_printer(to_attach, service_names, monochrome) + attach_to_logs(project, log_printer, service_names, timeout) def migrate_to_labels(self, project, _options): """ @@ -551,5 +540,26 @@ class TopLevelCommand(Command): print(get_version_info('full')) +def build_log_printer(containers, service_names, monochrome): + return LogPrinter( + [c for c in containers if c.service in service_names], + attach_params={"logs": True}, + monochrome=monochrome) + + +def attach_to_logs(project, log_printer, service_names, timeout): + print("Attaching to", list_containers(log_printer.containers)) + try: + log_printer.run() + finally: + def handler(signal, frame): + project.kill(service_names=service_names) + sys.exit(0) + signal.signal(signal.SIGINT, handler) + + print("Gracefully stopping... (press Ctrl+C again to force)") + project.stop(service_names=service_names, timeout=timeout) + + def list_containers(containers): return ", ".join(c.name for c in containers) diff --git a/compose/config.py b/compose/config.py index aa2db2c71..6bb0fea6a 100644 --- a/compose/config.py +++ b/compose/config.py @@ -12,9 +12,9 @@ from compose.cli.utils import find_candidates_in_parent_dirs DOCKER_CONFIG_KEYS = [ 'cap_add', 'cap_drop', + 'command', 'cpu_shares', 'cpuset', - 'command', 'detach', 'devices', 'dns', @@ -28,12 +28,12 @@ DOCKER_CONFIG_KEYS = [ 'image', 'labels', 'links', + 'log_driver', + 'log_opt', 'mac_address', 'mem_limit', 'memswap_limit', 'net', - 'log_driver', - 'log_opt', 'pid', 'ports', 'privileged', @@ -382,7 +382,7 @@ def parse_environment(environment): return dict(split_env(e) for e in environment) if isinstance(environment, dict): - return environment + return dict(environment) raise ConfigurationError( "environment \"%s\" must be a list or mapping," % @@ -440,12 +440,12 @@ def resolve_volume_path(volume, working_dir, service_name): if not any(host_path.startswith(c) for c in PATH_START_CHARS): log.warn( - 'Warning: the mapping "{0}" in the volumes config for ' - 'service "{1}" is ambiguous. In a future version of Docker, ' + 'Warning: the mapping "{0}:{1}" in the volumes config for ' + 'service "{2}" is ambiguous. In a future version of Docker, ' 'it will designate a "named" volume ' '(see https://github.com/docker/docker/pull/14242). ' - 'To prevent unexpected behaviour, change it to "./{0}"' - .format(volume, service_name) + 'To prevent unexpected behaviour, change it to "./{0}:{1}"' + .format(host_path, container_path, service_name) ) return "%s:%s" % (expand_path(working_dir, host_path), container_path) diff --git a/compose/container.py b/compose/container.py index 40aea98a4..6f88b41dc 100644 --- a/compose/container.py +++ b/compose/container.py @@ -62,9 +62,13 @@ class Container(object): def name(self): return self.dictionary['Name'][1:] + @property + def service(self): + return self.labels.get(LABEL_SERVICE) + @property def name_without_project(self): - return '{0}_{1}'.format(self.labels.get(LABEL_SERVICE), self.number) + return '{0}_{1}'.format(self.service, self.number) @property def number(self): diff --git a/compose/project.py b/compose/project.py index 6d86a4a87..abc3132a2 100644 --- a/compose/project.py +++ b/compose/project.py @@ -9,7 +9,10 @@ from .config import get_service_name_from_net, ConfigurationError from .const import DEFAULT_TIMEOUT, LABEL_PROJECT, LABEL_SERVICE, LABEL_ONE_OFF from .container import Container from .legacy import check_for_legacy_containers +from .service import ContainerNet +from .service import Net from .service import Service +from .service import ServiceNet from .utils import parallel_execute log = logging.getLogger(__name__) @@ -81,8 +84,14 @@ class Project(object): volumes_from = project.get_volumes_from(service_dict) net = project.get_net(service_dict) - project.services.append(Service(client=client, project=name, links=links, net=net, - volumes_from=volumes_from, **service_dict)) + project.services.append( + Service( + client=client, + project=name, + links=links, + net=net, + volumes_from=volumes_from, + **service_dict)) return project @property @@ -172,26 +181,26 @@ class Project(object): return volumes_from def get_net(self, service_dict): - if 'net' in service_dict: - net_name = get_service_name_from_net(service_dict.get('net')) + net = service_dict.pop('net', None) + if not net: + return Net(None) - if net_name: - try: - net = self.get_service(net_name) - except NoSuchService: - try: - net = Container.from_id(self.client, net_name) - except APIError: - raise ConfigurationError('Service "%s" is trying to use the network of "%s", which is not the name of a service or container.' % (service_dict['name'], net_name)) - else: - net = service_dict['net'] + net_name = get_service_name_from_net(net) + if not net_name: + return Net(net) - del service_dict['net'] - - else: - net = None - - return net + 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)) def start(self, service_names=None, **options): for service in self.get_services(service_names): diff --git a/compose/service.py b/compose/service.py index ab7d154e6..b3c68735c 100644 --- a/compose/service.py +++ b/compose/service.py @@ -83,7 +83,16 @@ ConvergencePlan = namedtuple('ConvergencePlan', 'action containers') class Service(object): - def __init__(self, name, client=None, project='default', links=None, external_links=None, volumes_from=None, net=None, **options): + def __init__( + self, + name, + client=None, + project='default', + links=None, + volumes_from=None, + net=None, + **options + ): if not re.match('^%s+$' % VALID_NAME_CHARS, name): raise ConfigError('Invalid service name "%s" - only %s are allowed' % (name, VALID_NAME_CHARS)) if not re.match('^%s+$' % VALID_NAME_CHARS, project): @@ -97,9 +106,8 @@ class Service(object): self.client = client self.project = project self.links = links or [] - self.external_links = external_links or [] self.volumes_from = volumes_from or [] - self.net = net or None + self.net = net or Net(None) self.options = options def containers(self, stopped=False, one_off=False): @@ -469,26 +477,26 @@ class Service(object): return { 'options': self.options, 'image_id': self.image()['Id'], + 'links': self.get_link_names(), + 'net': self.net.id, + 'volumes_from': self.get_volumes_from_names(), } def get_dependency_names(self): - net_name = self.get_net_name() - return (self.get_linked_names() + + net_name = self.net.service_name + return (self.get_linked_service_names() + self.get_volumes_from_names() + ([net_name] if net_name else [])) - def get_linked_names(self): - return [s.name for (s, _) in self.links] + def get_linked_service_names(self): + return [service.name for (service, _) in self.links] + + def get_link_names(self): + return [(service.name, alias) for service, alias in self.links] def get_volumes_from_names(self): return [s.name for s in self.volumes_from if isinstance(s, Service)] - def get_net_name(self): - if isinstance(self.net, Service): - return self.net.name - else: - return - def get_container_name(self, number, one_off=False): # TODO: Implement issue #652 here return build_container_name(self.project, self.name, number, one_off) @@ -517,7 +525,7 @@ class Service(object): links.append((container.name, self.name)) links.append((container.name, container.name)) links.append((container.name, container.name_without_project)) - for external_link in self.external_links: + for external_link in self.options.get('external_links') or []: if ':' not in external_link: link_name = external_link else: @@ -540,32 +548,12 @@ class Service(object): return volumes_from - def _get_net(self): - if not self.net: - return None - - if isinstance(self.net, Service): - containers = self.net.containers() - if len(containers) > 0: - net = 'container:' + containers[0].id - else: - log.warning("Warning: Service %s is trying to use reuse the network stack " - "of another service that is not running." % (self.net.name)) - net = None - elif isinstance(self.net, Container): - net = 'container:' + self.net.id - else: - net = self.net - - return net - def _get_container_create_options( self, override_options, number, one_off=False, previous_container=None): - add_config_hash = (not one_off and not override_options) container_options = dict( @@ -578,13 +566,6 @@ class Service(object): else: container_options['name'] = self.get_container_name(number, one_off) - if add_config_hash: - config_hash = self.config_hash() - if 'labels' not in container_options: - container_options['labels'] = {} - container_options['labels'][LABEL_CONFIG_HASH] = config_hash - log.debug("Added config hash: %s" % config_hash) - if 'detach' not in container_options: container_options['detach'] = True @@ -632,7 +613,8 @@ class Service(object): container_options['labels'] = build_container_labels( container_options.get('labels', {}), self.labels(one_off=one_off), - number) + number, + self.config_hash() if add_config_hash else None) # Delete options which are only used when starting for key in DOCKER_START_KEYS: @@ -679,7 +661,7 @@ class Service(object): binds=options.get('binds'), volumes_from=self._get_volumes_from(), privileged=privileged, - network_mode=self._get_net(), + network_mode=self.net.mode, devices=devices, dns=dns, dns_search=dns_search, @@ -774,6 +756,61 @@ class Service(object): stream_output(output, sys.stdout) +class Net(object): + """A `standard` network mode (ex: host, bridge)""" + + service_name = None + + def __init__(self, net): + self.net = net + + @property + def id(self): + return self.net + + mode = id + + +class ContainerNet(object): + """A network mode that uses a container's network stack.""" + + service_name = None + + def __init__(self, container): + self.container = container + + @property + def id(self): + return self.container.id + + @property + def mode(self): + return 'container:' + self.container.id + + +class ServiceNet(object): + """A network mode that uses a service's network stack.""" + + def __init__(self, service): + self.service = service + + @property + def id(self): + return self.service.name + + service_name = id + + @property + def mode(self): + containers = self.service.containers() + if containers: + return 'container:' + containers[0].id + + log.warn("Warning: Service %s is trying to use reuse the network stack " + "of another service that is not running." % (self.id)) + return None + + # Names @@ -899,11 +936,16 @@ def split_port(port): # Labels -def build_container_labels(label_options, service_labels, number, one_off=False): - labels = label_options or {} +def build_container_labels(label_options, service_labels, number, config_hash): + labels = dict(label_options or {}) labels.update(label.split('=', 1) for label in service_labels) labels[LABEL_CONTAINER_NUMBER] = str(number) labels[LABEL_VERSION] = __version__ + + if config_hash: + log.debug("Added config hash: %s" % config_hash) + labels[LABEL_CONFIG_HASH] = config_hash + return labels diff --git a/compose/utils.py b/compose/utils.py index 4c7f94c57..61d6d8024 100644 --- a/compose/utils.py +++ b/compose/utils.py @@ -32,6 +32,10 @@ def parallel_execute(objects, obj_callable, msg_index, msg): except APIError as e: errors[msg_index] = e.explanation result = "error" + except Exception as e: + errors[msg_index] = e + result = 'unexpected_exception' + q.put((msg_index, result)) for an_object in objects: @@ -48,6 +52,9 @@ def parallel_execute(objects, obj_callable, msg_index, msg): while done < total_to_execute: try: msg_index, result = q.get(timeout=1) + + if result == 'unexpected_exception': + raise errors[msg_index] if result == 'error': write_out_msg(stream, lines, msg_index, msg, status='error') else: diff --git a/docs/install.md b/docs/install.md index fa3679192..3daf4d944 100644 --- a/docs/install.md +++ b/docs/install.md @@ -53,7 +53,7 @@ To install Compose, do the following: 6. Test the installation. $ docker-compose --version - docker-compose version: 1.4.0 + docker-compose version: 1.4.1 ## Upgrading diff --git a/tests/__init__.py b/tests/__init__.py index 08a7865e9..c7a1bd4a0 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,5 +1,7 @@ import sys +import mock # noqa + if sys.version_info >= (2, 7): import unittest # NOQA else: diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index 9788c1861..a0fbe3e1f 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -112,7 +112,7 @@ class ProjectTest(DockerClientTestCase): web = project.get_service('web') net = project.get_service('net') - self.assertEqual(web._get_net(), 'container:' + net.containers()[0].id) + self.assertEqual(web.net.mode, 'container:' + net.containers()[0].id) def test_net_from_container(self): net_container = Container.create( @@ -138,7 +138,7 @@ class ProjectTest(DockerClientTestCase): project.up() web = project.get_service('web') - self.assertEqual(web._get_net(), 'container:' + net_container.id) + self.assertEqual(web.net.mode, 'container:' + net_container.id) def test_start_stop_kill_remove(self): web = self.create_service('web') diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 8856d0245..ec6428200 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -9,6 +9,7 @@ import tempfile import shutil from six import StringIO, text_type +from .testcases import DockerClientTestCase from compose import __version__ from compose.const import ( LABEL_CONTAINER_NUMBER, @@ -17,14 +18,12 @@ from compose.const import ( LABEL_SERVICE, LABEL_VERSION, ) -from compose.service import ( - ConfigError, - ConvergencePlan, - Service, - build_extra_hosts, -) from compose.container import Container -from .testcases import DockerClientTestCase +from compose.service import build_extra_hosts +from compose.service import ConfigError +from compose.service import ConvergencePlan +from compose.service import Net +from compose.service import Service def create_and_start_container(service, **override_options): @@ -672,6 +671,25 @@ class ServiceTest(DockerClientTestCase): self.assertTrue(service.containers()[0].is_running) self.assertIn("ERROR: for 2 Boom", mock_stdout.getvalue()) + @patch('sys.stdout', new_callable=StringIO) + def test_scale_with_api_returns_unexpected_exception(self, mock_stdout): + """ + Test that when scaling if the API returns an error, that is not of type + APIError, that error is re-raised. + """ + service = self.create_service('web') + next_number = service._next_container_number() + service.create_container(number=next_number, quiet=True) + + with patch( + 'compose.container.Container.create', + side_effect=ValueError("BOOM")): + with self.assertRaises(ValueError): + service.scale(3) + + self.assertEqual(len(service.containers()), 1) + self.assertTrue(service.containers()[0].is_running) + @patch('compose.service.log') def test_scale_with_desired_number_already_achieved(self, mock_log): """ @@ -724,17 +742,17 @@ class ServiceTest(DockerClientTestCase): self.assertEqual(list(container.inspect()['HostConfig']['PortBindings'].keys()), ['8000/tcp']) def test_network_mode_none(self): - service = self.create_service('web', net='none') + service = self.create_service('web', net=Net('none')) container = create_and_start_container(service) self.assertEqual(container.get('HostConfig.NetworkMode'), 'none') def test_network_mode_bridged(self): - service = self.create_service('web', net='bridge') + service = self.create_service('web', net=Net('bridge')) container = create_and_start_container(service) self.assertEqual(container.get('HostConfig.NetworkMode'), 'bridge') def test_network_mode_host(self): - service = self.create_service('web', net='host') + service = self.create_service('web', net=Net('host')) container = create_and_start_container(service) self.assertEqual(container.get('HostConfig.NetworkMode'), 'host') diff --git a/tests/integration/state_test.py b/tests/integration/state_test.py index b124b19ff..b25437617 100644 --- a/tests/integration/state_test.py +++ b/tests/integration/state_test.py @@ -1,3 +1,7 @@ +""" +Integration tests which cover state convergence (aka smart recreate) performed +by `docker-compose up`. +""" from __future__ import unicode_literals import tempfile import shutil @@ -151,6 +155,24 @@ class ProjectWithDependenciesTest(ProjectTestCase): self.assertEqual(new_containers - old_containers, set()) + def test_service_removed_while_down(self): + next_cfg = { + 'web': { + 'image': 'busybox:latest', + 'command': 'tail -f /dev/null', + }, + 'nginx': self.cfg['nginx'], + } + + containers = self.run_up(self.cfg) + self.assertEqual(len(containers), 3) + + project = self.make_project(self.cfg) + project.stop(timeout=1) + + containers = self.run_up(next_cfg) + self.assertEqual(len(containers), 2) + def converge(service, allow_recreate=True, diff --git a/tests/unit/cli/main_test.py b/tests/unit/cli/main_test.py new file mode 100644 index 000000000..817e8f49b --- /dev/null +++ b/tests/unit/cli/main_test.py @@ -0,0 +1,47 @@ +from __future__ import absolute_import + +from compose import container +from compose.cli.log_printer import LogPrinter +from compose.cli.main import attach_to_logs +from compose.cli.main import build_log_printer +from compose.project import Project +from tests import mock +from tests import unittest + + +def mock_container(service, number): + return mock.create_autospec( + container.Container, + service=service, + number=number, + name_without_project='{0}_{1}'.format(service, number)) + + +class CLIMainTestCase(unittest.TestCase): + + def test_build_log_printer(self): + containers = [ + mock_container('web', 1), + mock_container('web', 2), + mock_container('db', 1), + mock_container('other', 1), + mock_container('another', 1), + ] + service_names = ['web', 'db'] + log_printer = build_log_printer(containers, service_names, True) + self.assertEqual(log_printer.containers, containers[:3]) + + def test_attach_to_logs(self): + project = mock.create_autospec(Project) + log_printer = mock.create_autospec(LogPrinter, containers=[]) + service_names = ['web', 'db'] + timeout = 12 + + with mock.patch('compose.cli.main.signal', autospec=True) as mock_signal: + attach_to_logs(project, log_printer, service_names, timeout) + + mock_signal.signal.assert_called_once_with(mock_signal.SIGINT, mock.ANY) + log_printer.run.assert_called_once_with() + project.stop.assert_called_once_with( + service_names=service_names, + timeout=timeout) diff --git a/tests/unit/config_test.py b/tests/unit/config_test.py index c523798f2..cefb1a20f 100644 --- a/tests/unit/config_test.py +++ b/tests/unit/config_test.py @@ -512,7 +512,7 @@ class ExtendsTest(unittest.TestCase): We specify a 'file' key that is the filename we're already in. """ service_dicts = load_from_filename('tests/fixtures/extends/specify-file-as-self.yml') - self.assertEqual(service_dicts, [ + self.assertEqual(sorted(service_dicts), sorted([ { 'environment': { @@ -532,7 +532,7 @@ class ExtendsTest(unittest.TestCase): 'image': 'busybox', 'name': 'web' } - ]) + ])) def test_circular(self): try: diff --git a/tests/unit/project_test.py b/tests/unit/project_test.py index 93bf12ff5..a66aaf5d2 100644 --- a/tests/unit/project_test.py +++ b/tests/unit/project_test.py @@ -220,7 +220,7 @@ class ProjectTest(unittest.TestCase): } ], self.mock_client) service = project.get_service('test') - self.assertEqual(service._get_net(), None) + self.assertEqual(service.net.id, None) self.assertNotIn('NetworkMode', service._get_container_host_config({})) def test_use_net_from_container(self): @@ -235,7 +235,7 @@ class ProjectTest(unittest.TestCase): } ], self.mock_client) service = project.get_service('test') - self.assertEqual(service._get_net(), 'container:' + container_id) + self.assertEqual(service.net.mode, 'container:' + container_id) def test_use_net_from_service(self): container_name = 'test_aaa_1' @@ -260,7 +260,7 @@ class ProjectTest(unittest.TestCase): ], self.mock_client) service = project.get_service('test') - self.assertEqual(service._get_net(), 'container:' + container_name) + self.assertEqual(service.net.mode, 'container:' + container_name) def test_container_without_name(self): self.mock_client.containers.return_value = [ diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index 7e5266dd7..263c9b329 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -7,21 +7,25 @@ import mock import docker from docker.utils import LogConfig -from compose.service import Service +from compose.const import LABEL_CONFIG_HASH +from compose.const import LABEL_ONE_OFF +from compose.const import LABEL_PROJECT +from compose.const import LABEL_SERVICE from compose.container import Container -from compose.const import LABEL_SERVICE, LABEL_PROJECT, LABEL_ONE_OFF -from compose.service import ( - ConfigError, - NeedsBuildError, - NoSuchImageError, - build_port_bindings, - build_volume_binding, - get_container_data_volumes, - merge_volume_bindings, - parse_repository_tag, - parse_volume_spec, - split_port, -) +from compose.service import ConfigError +from compose.service import ContainerNet +from compose.service import NeedsBuildError +from compose.service import Net +from compose.service import NoSuchImageError +from compose.service import Service +from compose.service import ServiceNet +from compose.service import build_port_bindings +from compose.service import build_volume_binding +from compose.service import get_container_data_volumes +from compose.service import merge_volume_bindings +from compose.service import parse_repository_tag +from compose.service import parse_volume_spec +from compose.service import split_port class ServiceTest(unittest.TestCase): @@ -221,6 +225,40 @@ class ServiceTest(unittest.TestCase): self.assertEqual(opts['hostname'], 'name.sub', 'hostname') self.assertEqual(opts['domainname'], 'domain.tld', 'domainname') + def test_get_container_create_options_does_not_mutate_options(self): + labels = {'thing': 'real'} + environment = {'also': 'real'} + service = Service( + 'foo', + image='foo', + labels=dict(labels), + client=self.mock_client, + environment=dict(environment), + ) + self.mock_client.inspect_image.return_value = {'Id': 'abcd'} + prev_container = mock.Mock( + id='ababab', + image_config={'ContainerConfig': {}}) + + opts = service._get_container_create_options( + {}, + 1, + previous_container=prev_container) + + self.assertEqual(service.options['labels'], labels) + self.assertEqual(service.options['environment'], environment) + + self.assertEqual( + opts['labels'][LABEL_CONFIG_HASH], + '3c85881a8903b9d73a06c41860c8be08acce1494ab4cf8408375966dccd714de') + self.assertEqual( + opts['environment'], + { + 'affinity:container': '=ababab', + 'also': 'real', + } + ) + def test_get_container_not_found(self): self.mock_client.containers.return_value = [] service = Service('foo', client=self.mock_client, image='foo') @@ -340,6 +378,90 @@ class ServiceTest(unittest.TestCase): self.assertEqual(self.mock_client.build.call_count, 1) self.assertFalse(self.mock_client.build.call_args[1]['pull']) + def test_config_dict(self): + self.mock_client.inspect_image.return_value = {'Id': 'abcd'} + service = Service( + 'foo', + image='example.com/foo', + client=self.mock_client, + net=ServiceNet(Service('other', image='foo')), + links=[(Service('one', image='foo'), 'one')], + volumes_from=[Service('two', image='foo')]) + + config_dict = service.config_dict() + expected = { + 'image_id': 'abcd', + 'options': {'image': 'example.com/foo'}, + 'links': [('one', 'one')], + 'net': 'other', + 'volumes_from': ['two'], + } + self.assertEqual(config_dict, expected) + + def test_config_dict_with_net_from_container(self): + self.mock_client.inspect_image.return_value = {'Id': 'abcd'} + container = Container( + self.mock_client, + {'Id': 'aaabbb', 'Name': '/foo_1'}) + service = Service( + 'foo', + image='example.com/foo', + client=self.mock_client, + net=container) + + config_dict = service.config_dict() + expected = { + 'image_id': 'abcd', + 'options': {'image': 'example.com/foo'}, + 'links': [], + 'net': 'aaabbb', + 'volumes_from': [], + } + self.assertEqual(config_dict, expected) + + +class NetTestCase(unittest.TestCase): + + def test_net(self): + net = Net('host') + self.assertEqual(net.id, 'host') + self.assertEqual(net.mode, 'host') + self.assertEqual(net.service_name, None) + + def test_net_container(self): + container_id = 'abcd' + net = ContainerNet(Container(None, {'Id': container_id})) + self.assertEqual(net.id, container_id) + self.assertEqual(net.mode, 'container:' + container_id) + self.assertEqual(net.service_name, None) + + def test_net_service(self): + container_id = 'bbbb' + service_name = 'web' + mock_client = mock.create_autospec(docker.Client) + mock_client.containers.return_value = [ + {'Id': container_id, 'Name': container_id, 'Image': 'abcd'}, + ] + + service = Service(name=service_name, client=mock_client, image='foo') + net = ServiceNet(service) + + self.assertEqual(net.id, service_name) + self.assertEqual(net.mode, 'container:' + container_id) + self.assertEqual(net.service_name, service_name) + + def test_net_service_no_containers(self): + service_name = 'web' + mock_client = mock.create_autospec(docker.Client) + mock_client.containers.return_value = [] + + service = Service(name=service_name, client=mock_client, image='foo') + net = ServiceNet(service) + + self.assertEqual(net.id, service_name) + self.assertEqual(net.mode, None) + self.assertEqual(net.service_name, service_name) + def mock_get_image(images): if images: