diff --git a/compose/config/config.py b/compose/config/config.py index dbc6b6b22..d0024e9cd 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -612,6 +612,9 @@ def finalize_service(service_config, service_names, version): else: service_dict['network_mode'] = network_mode + if 'networks' in service_dict: + service_dict['networks'] = parse_networks(service_dict['networks']) + if 'restart' in service_dict: service_dict['restart'] = parse_restart_spec(service_dict['restart']) @@ -701,6 +704,7 @@ def merge_service_dicts(base, override, version): md.merge_mapping('environment', parse_environment) md.merge_mapping('labels', parse_labels) md.merge_mapping('ulimits', parse_ulimits) + md.merge_mapping('networks', parse_networks) md.merge_sequence('links', ServiceLink.parse) for field in ['volumes', 'devices']: @@ -710,7 +714,6 @@ def merge_service_dicts(base, override, version): 'depends_on', 'expose', 'external_links', - 'networks', 'ports', 'volumes_from', ]: @@ -798,6 +801,7 @@ def parse_dict_or_list(split_func, type_name, arguments): parse_build_arguments = functools.partial(parse_dict_or_list, split_env, 'build arguments') parse_environment = functools.partial(parse_dict_or_list, split_env, 'environment') parse_labels = functools.partial(parse_dict_or_list, split_label, 'labels') +parse_networks = functools.partial(parse_dict_or_list, lambda k: (k, None), 'networks') def parse_ulimits(ulimits): diff --git a/compose/config/service_schema_v2.0.json b/compose/config/service_schema_v2.0.json index 3196ca89e..edccedc66 100644 --- a/compose/config/service_schema_v2.0.json +++ b/compose/config/service_schema_v2.0.json @@ -120,11 +120,28 @@ "network_mode": {"type": "string"}, "networks": { - "type": "array", - "items": {"type": "string"}, - "uniqueItems": true + "oneOf": [ + {"$ref": "#/definitions/list_of_strings"}, + { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "oneOf": [ + { + "type": "object", + "properties": { + "aliases": {"$ref": "#/definitions/list_of_strings"} + }, + "additionalProperties": false + }, + {"type": "null"} + ] + } + }, + "additionalProperties": false + } + ] }, - "pid": {"type": ["string", "null"]}, "ports": { diff --git a/compose/network.py b/compose/network.py index 82a78f3b5..135502cc0 100644 --- a/compose/network.py +++ b/compose/network.py @@ -159,18 +159,26 @@ class ProjectNetworks(object): network.ensure() -def get_network_names_for_service(service_dict): +def get_network_aliases_for_service(service_dict): if 'network_mode' in service_dict: - return [] - return service_dict.get('networks', ['default']) + return {} + networks = service_dict.get('networks', {'default': None}) + return dict( + (net, (config or {}).get('aliases', [])) + for net, config in networks.items() + ) + + +def get_network_names_for_service(service_dict): + return get_network_aliases_for_service(service_dict).keys() def get_networks(service_dict, network_definitions): - networks = [] - for name in get_network_names_for_service(service_dict): + networks = {} + for name, aliases in get_network_aliases_for_service(service_dict).items(): network = network_definitions.get(name) if network: - networks.append(network.full_name) + networks[network.full_name] = aliases else: raise ConfigurationError( 'Service "{}" uses an undefined network "{}"' diff --git a/compose/project.py b/compose/project.py index 62e1d2cd3..cfb11aa05 100644 --- a/compose/project.py +++ b/compose/project.py @@ -69,11 +69,13 @@ class Project(object): if use_networking: service_networks = get_networks(service_dict, networks) else: - service_networks = [] + service_networks = {} service_dict.pop('networks', None) links = project.get_links(service_dict) - network_mode = project.get_network_mode(service_dict, service_networks) + network_mode = project.get_network_mode( + service_dict, list(service_networks.keys()) + ) volumes_from = get_volumes_from(project, service_dict) if config_data.version != V1: diff --git a/compose/service.py b/compose/service.py index 78eed4c46..8b22b7d75 100644 --- a/compose/service.py +++ b/compose/service.py @@ -124,7 +124,7 @@ class Service(object): self.links = links or [] self.volumes_from = volumes_from or [] self.network_mode = network_mode or NetworkMode(None) - self.networks = networks or [] + self.networks = networks or {} self.options = options def containers(self, stopped=False, one_off=False, filters={}): @@ -432,14 +432,14 @@ class Service(object): def connect_container_to_networks(self, container): connected_networks = container.get('NetworkSettings.Networks') - for network in self.networks: + for network, aliases in self.networks.items(): if network in connected_networks: self.client.disconnect_container_from_network( container.id, network) self.client.connect_container_to_network( container.id, network, - aliases=self._get_aliases(container), + aliases=list(self._get_aliases(container).union(aliases)), links=self._get_links(False), ) @@ -473,7 +473,7 @@ class Service(object): 'image_id': self.image()['Id'], 'links': self.get_link_names(), 'net': self.network_mode.id, - 'networks': self.networks, + 'networks': list(self.networks.keys()), 'volumes_from': [ (v.source.name, v.mode) for v in self.volumes_from if isinstance(v.source, Service) @@ -514,9 +514,9 @@ class Service(object): def _get_aliases(self, container): if container.labels.get(LABEL_ONE_OFF) == "True": - return [] + return set() - return [self.name, container.short_id] + return {self.name, container.short_id} def _get_links(self, link_to_self): links = {} diff --git a/docs/compose-file.md b/docs/compose-file.md index 04733916f..6441297fe 100644 --- a/docs/compose-file.md +++ b/docs/compose-file.md @@ -453,7 +453,7 @@ id. ### network_mode -> [Version 2 file format](#version-1) only. In version 1, use [net](#net). +> [Version 2 file format](#version-2) only. In version 1, use [net](#net). Network mode. Use the same values as the docker client `--net` parameter, plus the special form `service:[service name]`. @@ -475,6 +475,27 @@ Networks to join, referencing entries under the - some-network - other-network +#### aliases + +Aliases (alternative hostnames) for this service on the network. Other servers +on the network can use either the service name or this alias to connect to +this service. Since `alias` is network-scoped: + + * the same service can have different aliases when connected to another + network. + * it is allowable to configure the same alias name to multiple containers + (services) on the same network. + + + networks: + some-network: + aliases: + - alias1 + - alias3 + other-network: + aliases: + - alias2 + ### pid pid: "host" diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index ea3d132a5..318ab3d3f 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -185,7 +185,7 @@ class CLITestCase(DockerClientTestCase): 'build': { 'context': os.path.abspath(self.base_dir), }, - 'networks': ['front', 'default'], + 'networks': {'front': None, 'default': None}, 'volumes_from': ['service:other:rw'], }, 'other': { @@ -445,6 +445,34 @@ class CLITestCase(DockerClientTestCase): assert networks[0]['Options']['com.docker.network.bridge.enable_icc'] == 'false' + @v2_only() + def test_up_with_network_aliases(self): + filename = 'network-aliases.yml' + self.base_dir = 'tests/fixtures/networks' + self.dispatch(['-f', filename, 'up', '-d'], None) + back_name = '{}_back'.format(self.project.name) + front_name = '{}_front'.format(self.project.name) + + networks = [ + n for n in self.client.networks() + if n['Name'].startswith('{}_'.format(self.project.name)) + ] + + # Two networks were created: back and front + assert sorted(n['Name'] for n in networks) == [back_name, front_name] + web_container = self.project.get_service('web').containers()[0] + + back_aliases = web_container.get( + 'NetworkSettings.Networks.{}.Aliases'.format(back_name) + ) + assert 'web' in back_aliases + front_aliases = web_container.get( + 'NetworkSettings.Networks.{}.Aliases'.format(front_name) + ) + assert 'web' in front_aliases + assert 'forward_facing' in front_aliases + assert 'ahead' in front_aliases + @v2_only() def test_up_with_networks(self): self.base_dir = 'tests/fixtures/networks' diff --git a/tests/fixtures/networks/network-aliases.yml b/tests/fixtures/networks/network-aliases.yml new file mode 100644 index 000000000..8cf7d5af9 --- /dev/null +++ b/tests/fixtures/networks/network-aliases.yml @@ -0,0 +1,16 @@ +version: "2" + +services: + web: + image: busybox + command: top + networks: + front: + aliases: + - forward_facing + - ahead + back: + +networks: + front: {} + back: {} diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index 6bb076a3f..6542fa18e 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -565,7 +565,7 @@ class ProjectTest(DockerClientTestCase): 'name': 'web', 'image': 'busybox:latest', 'command': 'top', - 'networks': ['foo', 'bar', 'baz'], + 'networks': {'foo': None, 'bar': None, 'baz': None}, }], volumes={}, networks={ @@ -598,7 +598,7 @@ class ProjectTest(DockerClientTestCase): services=[{ 'name': 'web', 'image': 'busybox:latest', - 'networks': ['front'], + 'networks': {'front': None}, }], volumes={}, networks={ diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 1d6f1cbb0..204003bce 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -649,6 +649,42 @@ class ConfigTest(unittest.TestCase): assert service['build']['args']['opt1'] == '42' assert service['build']['args']['opt2'] == 'foobar' + def test_load_with_multiple_files_mismatched_networks_format(self): + base_file = config.ConfigFile( + 'base.yaml', + { + 'version': '2', + 'services': { + 'web': { + 'image': 'example/web', + 'networks': { + 'foobar': {'aliases': ['foo', 'bar']} + } + } + }, + 'networks': {'foobar': {}, 'baz': {}} + } + ) + + override_file = config.ConfigFile( + 'override.yaml', + { + 'version': '2', + 'services': { + 'web': { + 'networks': ['baz'] + } + } + } + ) + + details = config.ConfigDetails('.', [base_file, override_file]) + web_service = config.load(details).services[0] + assert web_service['networks'] == { + 'foobar': {'aliases': ['foo', 'bar']}, + 'baz': None + } + def test_load_with_multiple_files_v2(self): base_file = config.ConfigFile( 'base.yaml', diff --git a/tests/unit/project_test.py b/tests/unit/project_test.py index bec238de6..c28c21523 100644 --- a/tests/unit/project_test.py +++ b/tests/unit/project_test.py @@ -438,7 +438,7 @@ class ProjectTest(unittest.TestCase): { 'name': 'foo', 'image': 'busybox:latest', - 'networks': ['custom'] + 'networks': {'custom': None} }, ], networks={'custom': {}},