From ee136446a2e5d1a2b108f586e872f40d801485d6 Mon Sep 17 00:00:00 2001 From: Matt Daue Date: Tue, 23 Feb 2016 21:19:11 -0500 Subject: [PATCH] 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 --- compose/config/config_schema_v2.0.json | 4 +- compose/network.py | 10 +- compose/service.py | 9 +- docs/networking.md | 24 +++++ requirements.txt | 2 +- tests/acceptance/cli_test.py | 24 +++++ .../networks/network-static-addresses.yml | 23 +++++ tests/integration/project_test.py | 91 +++++++++++++++++++ 8 files changed, 178 insertions(+), 9 deletions(-) create mode 100755 tests/fixtures/networks/network-static-addresses.yml diff --git a/compose/config/config_schema_v2.0.json b/compose/config/config_schema_v2.0.json index a4a30a5f4..33afc9b2c 100644 --- a/compose/config/config_schema_v2.0.json +++ b/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 }, diff --git a/compose/network.py b/compose/network.py index 135502cc0..81e3b5bf3 100644 --- a/compose/network.py +++ b/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 "{}"' diff --git a/compose/service.py b/compose/service.py index 7ee441f2c..fad1c4d93 100644 --- a/compose/service.py +++ b/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): diff --git a/docs/networking.md b/docs/networking.md index 1fd6c1161..e38e56902 100644 --- a/docs/networking.md +++ b/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) diff --git a/requirements.txt b/requirements.txt index b31840c85..074864d47 100644 --- a/requirements.txt +++ b/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 diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 24682125e..c94578a1b 100644 --- a/tests/acceptance/cli_test.py +++ b/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' diff --git a/tests/fixtures/networks/network-static-addresses.yml b/tests/fixtures/networks/network-static-addresses.yml new file mode 100755 index 000000000..f820ff6a4 --- /dev/null +++ b/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 diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index daeb9c81d..710da9a32 100644 --- a/tests/integration/project_test.py +++ b/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))