소스 검색

Merge pull request #2907 from shin-/2829-net-alias

Allow user to specify custom network aliases
Joffrey F 9 년 전
부모
커밋
5fc0df4be2

+ 5 - 1
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):

+ 21 - 4
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": {

+ 14 - 6
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 "{}"'

+ 4 - 2
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:

+ 6 - 6
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 = {}

+ 22 - 1
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"

+ 29 - 1
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'

+ 16 - 0
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: {}

+ 2 - 2
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={

+ 36 - 0
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',

+ 1 - 1
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': {}},