Browse Source

Fix #2804: Add ipv4 and ipv6 static addressing

- Added ipv4_network and ipv6_network to the networks section in the
  service section for each configured network
- Added feature documentation
- Added unit tests

Signed-off-by: Matt Daue <[email protected]>
Matt Daue 9 years ago
parent
commit
ee136446a2

+ 3 - 1
compose/config/config_schema_v2.0.json

@@ -152,7 +152,9 @@
                     {
                       "type": "object",
                       "properties": {
-                        "aliases": {"$ref": "#/definitions/list_of_strings"}
+                        "aliases": {"$ref": "#/definitions/list_of_strings"},
+                        "ipv4_address": {"type": "string"},
+                        "ipv6_address": {"type": "string"}
                       },
                       "additionalProperties": false
                     },

+ 5 - 5
compose/network.py

@@ -159,26 +159,26 @@ class ProjectNetworks(object):
             network.ensure()
 
 
-def get_network_aliases_for_service(service_dict):
+def get_network_defs_for_service(service_dict):
     if 'network_mode' in service_dict:
         return {}
     networks = service_dict.get('networks', {'default': None})
     return dict(
-        (net, (config or {}).get('aliases', []))
+        (net, (config or {}))
         for net, config in networks.items()
     )
 
 
 def get_network_names_for_service(service_dict):
-    return get_network_aliases_for_service(service_dict).keys()
+    return get_network_defs_for_service(service_dict).keys()
 
 
 def get_networks(service_dict, network_definitions):
     networks = {}
-    for name, aliases in get_network_aliases_for_service(service_dict).items():
+    for name, netdef in get_network_defs_for_service(service_dict).items():
         network = network_definitions.get(name)
         if network:
-            networks[network.full_name] = aliases
+            networks[network.full_name] = netdef
         else:
             raise ConfigurationError(
                 'Service "{}" uses an undefined network "{}"'

+ 7 - 2
compose/service.py

@@ -451,7 +451,10 @@ class Service(object):
     def connect_container_to_networks(self, container):
         connected_networks = container.get('NetworkSettings.Networks')
 
-        for network, aliases in self.networks.items():
+        for network, netdefs in self.networks.items():
+            aliases = netdefs.get('aliases', [])
+            ipv4_address = netdefs.get('ipv4_address', None)
+            ipv6_address = netdefs.get('ipv6_address', None)
             if network in connected_networks:
                 self.client.disconnect_container_from_network(
                     container.id, network)
@@ -459,7 +462,9 @@ class Service(object):
             self.client.connect_container_to_network(
                 container.id, network,
                 aliases=list(self._get_aliases(container).union(aliases)),
-                links=self._get_links(False),
+                ipv4_address=ipv4_address,
+                ipv6_address=ipv6_address,
+                links=self._get_links(False)
             )
 
     def remove_duplicate_containers(self, timeout=DEFAULT_TIMEOUT):

+ 24 - 0
docs/networking.md

@@ -116,6 +116,30 @@ Here's an example Compose file defining two custom networks. The `proxy` service
           foo: "1"
           bar: "2"
 
+Networks can be configured with static IP addresses by setting the ipv4_address and/or ipv6_address for each attached network. The corresponding `network` section must have an `ipam` config entry with subnet and gateway configurations for each static address. If IPv6 addressing is desired, the `com.docker.network.enable_ipv6` driver option must be set to `true`. An example:
+
+    version: '2'
+
+    services:
+      app:
+        networks:
+          app_net:
+            ipv4_address: 172.16.238.10
+            ipv6_address: 2001:3984:3989::10
+
+    networks:
+      app_net:
+        driver: bridge
+        driver_opts:
+          com.docker.network.enable_ipv6: "true"
+        ipam:
+          driver: default
+          config:
+          - subnet: 172.16.238.0/24
+            gateway: 172.16.238.1
+          - subnet: 2001:3984:3989::/64
+            gateway: 2001:3984:3989::1
+
 For full details of the network configuration options available, see the following references:
 
 - [Top-level `networks` key](compose-file.md#network-configuration-reference)

+ 1 - 1
requirements.txt

@@ -3,7 +3,7 @@ cached-property==1.2.0
 dockerpty==0.4.1
 docopt==0.6.1
 enum34==1.0.4
-git+https://github.com/docker/docker-py.git@81d8caaf36159bf1accd86eab2e157bf8dd071a9#egg=docker-py
+git+https://github.com/docker/docker-py.git@d8be3e0fce60fbe25be088b64bccbcee83effdb1#egg=docker-py
 jsonschema==2.5.1
 requests==2.7.0
 six==1.7.3

+ 24 - 0
tests/acceptance/cli_test.py

@@ -475,6 +475,30 @@ class CLITestCase(DockerClientTestCase):
         assert 'forward_facing' in front_aliases
         assert 'ahead' in front_aliases
 
+    @v2_only()
+    def test_up_with_network_static_addresses(self):
+        filename = 'network-static-addresses.yml'
+        ipv4_address = '172.16.100.100'
+        ipv6_address = 'fe80::1001:100'
+        self.base_dir = 'tests/fixtures/networks'
+        self.dispatch(['-f', filename, 'up', '-d'], None)
+        static_net = '{}_static_test'.format(self.project.name)
+
+        networks = [
+            n for n in self.client.networks()
+            if n['Name'].startswith('{}_'.format(self.project.name))
+        ]
+
+        # One networks was created: front
+        assert sorted(n['Name'] for n in networks) == [static_net]
+        web_container = self.project.get_service('web').containers()[0]
+
+        ipam_config = web_container.get(
+            'NetworkSettings.Networks.{}.IPAMConfig'.format(static_net)
+        )
+        assert ipv4_address in ipam_config.values()
+        assert ipv6_address in ipam_config.values()
+
     @v2_only()
     def test_up_with_networks(self):
         self.base_dir = 'tests/fixtures/networks'

+ 23 - 0
tests/fixtures/networks/network-static-addresses.yml

@@ -0,0 +1,23 @@
+version: "2"
+
+services:
+  web:
+    image: busybox
+    command: top
+    networks:
+      static_test:
+        ipv4_address: 172.16.100.100
+        ipv6_address: fe80::1001:100
+
+networks:
+  static_test:
+    driver: bridge
+    driver_opts:
+      com.docker.network.enable_ipv6: "true"
+    ipam:
+      driver: default
+      config:
+      - subnet: 172.16.100.0/24
+        gateway: 172.16.100.1
+      - subnet: fe80::/64
+        gateway: fe80::1001:1

+ 91 - 0
tests/integration/project_test.py

@@ -5,6 +5,7 @@ import random
 
 import py
 import pytest
+from docker.errors import APIError
 from docker.errors import NotFound
 
 from ..helpers import build_config
@@ -650,6 +651,96 @@ class ProjectTest(DockerClientTestCase):
             }],
         }
 
+    @v2_only()
+    def test_up_with_network_static_addresses(self):
+        config_data = config.Config(
+            version=V2_0,
+            services=[{
+                'name': 'web',
+                'image': 'busybox:latest',
+                'networks': {
+                    'static_test': {
+                        'ipv4_address': '172.16.100.100',
+                        'ipv6_address': 'fe80::1001:102'
+                    }
+                },
+            }],
+            volumes={},
+            networks={
+                'static_test': {
+                    'driver': 'bridge',
+                    'driver_opts': {
+                        "com.docker.network.enable_ipv6": "true",
+                    },
+                    'ipam': {
+                        'driver': 'default',
+                        'config': [
+                            {"subnet": "172.16.100.0/24",
+                             "gateway": "172.16.100.1"},
+                            {"subnet": "fe80::/64",
+                             "gateway": "fe80::1001:1"}
+                        ]
+                    }
+                }
+            }
+        )
+        project = Project.from_config(
+            client=self.client,
+            name='composetest',
+            config_data=config_data,
+        )
+        project.up()
+
+        network = self.client.networks(names=['static_test'])[0]
+        service_container = project.get_service('web').containers()[0]
+
+        assert network['Options'] == {
+            "com.docker.network.enable_ipv6": "true"
+        }
+
+        IPAMConfig = (service_container.inspect().get('NetworkSettings', {}).
+                      get('Networks', {}).get('composetest_static_test', {}).
+                      get('IPAMConfig', {}))
+        assert IPAMConfig.get('IPv4Address') == '172.16.100.100'
+        assert IPAMConfig.get('IPv6Address') == 'fe80::1001:102'
+
+    @v2_only()
+    def test_up_with_network_static_addresses_missing_subnet(self):
+        config_data = config.Config(
+            version=V2_0,
+            services=[{
+                'name': 'web',
+                'image': 'busybox:latest',
+                'networks': {
+                    'static_test': {
+                        'ipv4_address': '172.16.100.100',
+                        'ipv6_address': 'fe80::1001:101'
+                    }
+                },
+            }],
+            volumes={},
+            networks={
+                'static_test': {
+                    'driver': 'bridge',
+                    'driver_opts': {
+                        "com.docker.network.enable_ipv6": "true",
+                    },
+                    'ipam': {
+                        'driver': 'default',
+                    },
+                },
+            },
+        )
+
+        project = Project.from_config(
+            client=self.client,
+            name='composetest',
+            config_data=config_data,
+        )
+
+        with self.assertRaises(APIError):
+            project.up()
+
     @v2_only()
     def test_project_up_volumes(self):
         vol_name = '{0:x}'.format(random.getrandbits(32))