Selaa lähdekoodia

Merge pull request #2743 from aanand/network-mode

Implement network_mode option
Joffrey F 9 vuotta sitten
vanhempi
sitoutus
4b84d088e0

+ 18 - 2
compose/config/config.py

@@ -19,7 +19,8 @@ from .errors import CircularReference
 from .errors import ComposeFileNotFound
 from .errors import ConfigurationError
 from .interpolation import interpolate_environment_variables
-from .sort_services import get_service_name_from_net
+from .sort_services import get_container_name_from_network_mode
+from .sort_services import get_service_name_from_network_mode
 from .sort_services import sort_service_dicts
 from .types import parse_extra_hosts
 from .types import parse_restart_spec
@@ -30,6 +31,7 @@ from .validation import validate_against_fields_schema
 from .validation import validate_against_service_schema
 from .validation import validate_depends_on
 from .validation import validate_extends_file_path
+from .validation import validate_network_mode
 from .validation import validate_top_level_object
 from .validation import validate_top_level_service_objects
 from .validation import validate_ulimits
@@ -490,10 +492,15 @@ def validate_extended_service_dict(service_dict, filename, service):
             "%s services with 'volumes_from' cannot be extended" % error_prefix)
 
     if 'net' in service_dict:
-        if get_service_name_from_net(service_dict['net']) is not None:
+        if get_container_name_from_network_mode(service_dict['net']):
             raise ConfigurationError(
                 "%s services with 'net: container' cannot be extended" % error_prefix)
 
+    if 'network_mode' in service_dict:
+        if get_service_name_from_network_mode(service_dict['network_mode']):
+            raise ConfigurationError(
+                "%s services with 'network_mode: service' cannot be extended" % error_prefix)
+
     if 'depends_on' in service_dict:
         raise ConfigurationError(
             "%s services with 'depends_on' cannot be extended" % error_prefix)
@@ -505,6 +512,7 @@ def validate_service(service_config, service_names, version):
     validate_paths(service_dict)
 
     validate_ulimits(service_config)
+    validate_network_mode(service_config, service_names)
     validate_depends_on(service_config, service_names)
 
     if not service_dict.get('image') and has_uppercase(service_name):
@@ -565,6 +573,14 @@ def finalize_service(service_config, service_names, version):
         service_dict['volumes'] = [
             VolumeSpec.parse(v) for v in service_dict['volumes']]
 
+    if 'net' in service_dict:
+        network_mode = service_dict.pop('net')
+        container_name = get_container_name_from_network_mode(network_mode)
+        if container_name and container_name in service_names:
+            service_dict['network_mode'] = 'service:{}'.format(container_name)
+        else:
+            service_dict['network_mode'] = network_mode
+
     if 'restart' in service_dict:
         service_dict['restart'] = parse_restart_spec(service_dict['restart'])
 

+ 1 - 0
compose/config/service_schema_v2.json

@@ -103,6 +103,7 @@
         "mac_address": {"type": "string"},
         "mem_limit": {"type": ["number", "string"]},
         "memswap_limit": {"type": ["number", "string"]},
+        "network_mode": {"type": "string"},
 
         "networks": {
           "type": "array",

+ 13 - 5
compose/config/sort_services.py

@@ -4,14 +4,22 @@ from __future__ import unicode_literals
 from compose.config.errors import DependencyError
 
 
-def get_service_name_from_net(net_config):
-    if not net_config:
+def get_service_name_from_network_mode(network_mode):
+    return get_source_name_from_network_mode(network_mode, 'service')
+
+
+def get_container_name_from_network_mode(network_mode):
+    return get_source_name_from_network_mode(network_mode, 'container')
+
+
+def get_source_name_from_network_mode(network_mode, source_type):
+    if not network_mode:
         return
 
-    if not net_config.startswith('container:'):
+    if not network_mode.startswith(source_type+':'):
         return
 
-    _, net_name = net_config.split(':', 1)
+    _, net_name = network_mode.split(':', 1)
     return net_name
 
 
@@ -33,7 +41,7 @@ def sort_service_dicts(services):
             service for service in services
             if (name in get_service_names(service.get('links', [])) or
                 name in get_service_names_from_volumes_from(service.get('volumes_from', [])) or
-                name == get_service_name_from_net(service.get('net')) or
+                name == get_service_name_from_network_mode(service.get('network_mode')) or
                 name in service.get('depends_on', []))
         ]
 

+ 19 - 0
compose/config/validation.py

@@ -15,6 +15,7 @@ from jsonschema import RefResolver
 from jsonschema import ValidationError
 
 from .errors import ConfigurationError
+from .sort_services import get_service_name_from_network_mode
 
 
 log = logging.getLogger(__name__)
@@ -147,6 +148,24 @@ def validate_extends_file_path(service_name, extends_options, filename):
         )
 
 
+def validate_network_mode(service_config, service_names):
+    network_mode = service_config.config.get('network_mode')
+    if not network_mode:
+        return
+
+    if 'networks' in service_config.config:
+        raise ConfigurationError("'network_mode' and 'networks' cannot be combined")
+
+    dependency = get_service_name_from_network_mode(network_mode)
+    if not dependency:
+        return
+
+    if dependency not in service_names:
+        raise ConfigurationError(
+            "Service '{s.name}' uses the network stack of service '{dep}' which "
+            "is undefined.".format(s=service_config, dep=dependency))
+
+
 def validate_depends_on(service_config, service_names):
     for dependency in service_config.config.get('depends_on', []):
         if dependency not in service_names:

+ 30 - 27
compose/project.py

@@ -10,7 +10,8 @@ from docker.errors import NotFound
 
 from . import parallel
 from .config import ConfigurationError
-from .config.sort_services import get_service_name_from_net
+from .config.sort_services import get_container_name_from_network_mode
+from .config.sort_services import get_service_name_from_network_mode
 from .const import DEFAULT_TIMEOUT
 from .const import IMAGE_EVENTS
 from .const import LABEL_ONE_OFF
@@ -18,11 +19,11 @@ from .const import LABEL_PROJECT
 from .const import LABEL_SERVICE
 from .container import Container
 from .network import Network
-from .service import ContainerNet
+from .service import ContainerNetworkMode
 from .service import ConvergenceStrategy
-from .service import Net
+from .service import NetworkMode
 from .service import Service
-from .service import ServiceNet
+from .service import ServiceNetworkMode
 from .utils import microseconds_from_time_nano
 from .volume import Volume
 
@@ -86,12 +87,11 @@ class Project(object):
         for service_dict in config_data.services:
             if use_networking:
                 networks = get_networks(service_dict, all_networks)
-                net = Net(networks[0]) if networks else Net("none")
             else:
                 networks = []
-                net = project.get_net(service_dict)
 
             links = project.get_links(service_dict)
+            network_mode = project.get_network_mode(service_dict, networks)
             volumes_from = get_volumes_from(project, service_dict)
 
             if config_data.version == 2:
@@ -110,7 +110,7 @@ class Project(object):
                     use_networking=use_networking,
                     networks=networks,
                     links=links,
-                    net=net,
+                    network_mode=network_mode,
                     volumes_from=volumes_from,
                     **service_dict)
             )
@@ -197,27 +197,27 @@ class Project(object):
             del service_dict['links']
         return links
 
-    def get_net(self, service_dict):
-        net = service_dict.pop('net', None)
-        if not net:
-            return Net(None)
+    def get_network_mode(self, service_dict, networks):
+        network_mode = service_dict.pop('network_mode', None)
+        if not network_mode:
+            if self.use_networking:
+                return NetworkMode(networks[0]) if networks else NetworkMode('none')
+            return NetworkMode(None)
 
-        net_name = get_service_name_from_net(net)
-        if not net_name:
-            return Net(net)
+        service_name = get_service_name_from_network_mode(network_mode)
+        if service_name:
+            return ServiceNetworkMode(self.get_service(service_name))
 
-        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))
+        container_name = get_container_name_from_network_mode(network_mode)
+        if container_name:
+            try:
+                return ContainerNetworkMode(Container.from_id(self.client, container_name))
+            except APIError:
+                raise ConfigurationError(
+                    "Service '{name}' uses the network stack of container '{dep}' which "
+                    "does not exist.".format(name=service_dict['name'], dep=container_name))
+
+        return NetworkMode(network_mode)
 
     def start(self, service_names=None, **options):
         containers = []
@@ -465,9 +465,12 @@ class Project(object):
 
 
 def get_networks(service_dict, network_definitions):
+    if 'network_mode' in service_dict:
+        return []
+
     networks = []
     for name in service_dict.pop('networks', ['default']):
-        if name in ['bridge', 'host']:
+        if name in ['bridge']:
             networks.append(name)
         else:
             matches = [n for n in network_definitions if n.name == name]

+ 11 - 12
compose/service.py

@@ -47,7 +47,6 @@ DOCKER_START_KEYS = [
     'extra_hosts',
     'ipc',
     'read_only',
-    'net',
     'log_driver',
     'log_opt',
     'mem_limit',
@@ -113,7 +112,7 @@ class Service(object):
         use_networking=False,
         links=None,
         volumes_from=None,
-        net=None,
+        network_mode=None,
         networks=None,
         **options
     ):
@@ -123,7 +122,7 @@ class Service(object):
         self.use_networking = use_networking
         self.links = links or []
         self.volumes_from = volumes_from or []
-        self.net = net or Net(None)
+        self.network_mode = network_mode or NetworkMode(None)
         self.networks = networks or []
         self.options = options
 
@@ -472,7 +471,7 @@ class Service(object):
             'options': self.options,
             'image_id': self.image()['Id'],
             'links': self.get_link_names(),
-            'net': self.net.id,
+            'net': self.network_mode.id,
             'volumes_from': [
                 (v.source.name, v.mode)
                 for v in self.volumes_from if isinstance(v.source, Service)
@@ -480,7 +479,7 @@ class Service(object):
         }
 
     def get_dependency_names(self):
-        net_name = self.net.service_name
+        net_name = self.network_mode.service_name
         return (self.get_linked_service_names() +
                 self.get_volumes_from_names() +
                 ([net_name] if net_name else []) +
@@ -636,7 +635,7 @@ class Service(object):
             binds=options.get('binds'),
             volumes_from=self._get_volumes_from(),
             privileged=options.get('privileged', False),
-            network_mode=self.net.mode,
+            network_mode=self.network_mode.mode,
             devices=options.get('devices'),
             dns=options.get('dns'),
             dns_search=options.get('dns_search'),
@@ -774,22 +773,22 @@ class Service(object):
                 log.error(six.text_type(e))
 
 
-class Net(object):
+class NetworkMode(object):
     """A `standard` network mode (ex: host, bridge)"""
 
     service_name = None
 
-    def __init__(self, net):
-        self.net = net
+    def __init__(self, network_mode):
+        self.network_mode = network_mode
 
     @property
     def id(self):
-        return self.net
+        return self.network_mode
 
     mode = id
 
 
-class ContainerNet(object):
+class ContainerNetworkMode(object):
     """A network mode that uses a container's network stack."""
 
     service_name = None
@@ -806,7 +805,7 @@ class ContainerNet(object):
         return 'container:' + self.container.id
 
 
-class ServiceNet(object):
+class ServiceNetworkMode(object):
     """A network mode that uses a service's network stack."""
 
     def __init__(self, service):

+ 35 - 14
docs/compose-file.md

@@ -437,14 +437,29 @@ Specify logging options as key-value pairs. An example of `syslog` options:
 ### net
 
 > [Version 1 file format](#version-1) only. In version 2, use
-> [networks](#networks).
+> [network_mode](#network_mode).
 
-Networking mode. Use the same values as the docker client `--net` parameter.
+Network mode. Use the same values as the docker client `--net` parameter.
+The `container:...` form can take a service name instead of a container name or
+id.
 
     net: "bridge"
-    net: "none"
-    net: "container:[name or id]"
     net: "host"
+    net: "none"
+    net: "container:[service name or container name/id]"
+
+### network_mode
+
+> [Version 2 file format](#version-1) 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]`.
+
+    network_mode: "bridge"
+    network_mode: "host"
+    network_mode: "none"
+    network_mode: "service:[service name]"
+    network_mode: "container:[container name/id]"
 
 ### networks
 
@@ -457,8 +472,8 @@ Networks to join, referencing entries under the
       - some-network
       - other-network
 
-The values `bridge`, `host` and `none` can also be used, and are equivalent to
-`net: "bridge"`, `net: "host"` or `net: "none"` in version 1.
+The value `bridge` can also be used to make containers join the pre-defined
+`bridge` network.
 
 There is no equivalent to `net: "container:[name or id]"`.
 
@@ -918,16 +933,22 @@ It's more complicated if you're using particular configuration features:
     your service's containers to an
     [external network](networking.md#using-a-pre-existing-network).
 
--   `net`: If you're using `host`, `bridge` or `none`, this is now replaced by
-    `networks`:
+-   `net`: This is now replaced by [network_mode](#network_mode):
+
+        net: host    ->  network_mode: host
+        net: bridge  ->  network_mode: bridge
+        net: none    ->  network_mode: none
+
+    If you're using `net: "container:[service name]"`, you must now use
+    `network_mode: "service:[service name]"` instead.
+
+        net: "container:web"  ->  network_mode: "service:web"
 
-        net: host    ->  networks: ["host"]
-        net: bridge  ->  networks: ["bridge"]
-        net: none    ->  networks: ["none"]
+    If you're using `net: "container:[container name/id]"`, the value does not
+    need to change.
 
-    If you're using `net: "container:<name>"`, there is no equivalent to this in
-    version 2 - you should use [Docker networks](networking.md) for
-    communication instead.
+        net: "container:cont-name"  ->  network_mode: "container:cont-name"
+        net: "container:abc12345"   ->  network_mode: "container:abc12345"
 
 
 ## Variable substitution

+ 0 - 12
docs/networking.md

@@ -144,15 +144,3 @@ If you want your containers to join a pre-existing network, use the [`external`
           name: my-pre-existing-network
 
 Instead of attemping to create a network called `[projectname]_default`, Compose will look for a network called `my-pre-existing-network` and connect your app's containers to it.
-
-## Custom container network modes
-
-The `docker` CLI command allows you to specify a custom network mode for a container with the `--net` option - for example, `--net=host` specifies that the container should use the same network namespace as the Docker host, and `--net=none` specifies that it should have no networking capabilities.
-
-To make use of this in Compose, specify a `networks` list with a single item `host`, `bridge` or `none`:
-
-    app:
-      build: ./app
-      networks: ["host"]
-
-There is no equivalent to `--net=container:CONTAINER_NAME` in the v2 Compose file format. You should instead use networks to enable communication.

+ 33 - 2
tests/acceptance/cli_test.py

@@ -496,8 +496,29 @@ class CLITestCase(DockerClientTestCase):
         assert 'Service "web" uses an undefined network "foo"' in result.stderr
 
     @v2_only()
-    def test_up_predefined_networks(self):
-        filename = 'predefined-networks.yml'
+    def test_up_with_bridge_network_plus_default(self):
+        filename = 'bridge.yml'
+
+        self.base_dir = 'tests/fixtures/networks'
+        self._project = get_project(self.base_dir, [filename])
+
+        self.dispatch(['-f', filename, 'up', '-d'], None)
+
+        container = self.project.containers()[0]
+
+        assert sorted(list(container.get('NetworkSettings.Networks'))) == sorted([
+            'bridge',
+            self.project.default_network.full_name,
+        ])
+
+    @v2_only()
+    def test_up_with_network_mode(self):
+        c = self.client.create_container('busybox', 'top', name='composetest_network_mode_container')
+        self.addCleanup(self.client.remove_container, c, force=True)
+        self.client.start(c)
+        container_mode_source = 'container:{}'.format(c['Id'])
+
+        filename = 'network-mode.yml'
 
         self.base_dir = 'tests/fixtures/networks'
         self._project = get_project(self.base_dir, [filename])
@@ -515,6 +536,16 @@ class CLITestCase(DockerClientTestCase):
             assert list(container.get('NetworkSettings.Networks')) == [name]
             assert container.get('HostConfig.NetworkMode') == name
 
+        service_mode_source = 'container:{}'.format(
+            self.project.get_service('bridge').containers()[0].id)
+        service_mode_container = self.project.get_service('service').containers()[0]
+        assert not service_mode_container.get('NetworkSettings.Networks')
+        assert service_mode_container.get('HostConfig.NetworkMode') == service_mode_source
+
+        container_mode_container = self.project.get_service('container').containers()[0]
+        assert not container_mode_container.get('NetworkSettings.Networks')
+        assert container_mode_container.get('HostConfig.NetworkMode') == container_mode_source
+
     @v2_only()
     def test_up_external_networks(self):
         filename = 'external-networks.yml'

+ 1 - 0
tests/fixtures/extends/common.yml

@@ -1,6 +1,7 @@
 web:
   image: busybox
   command: /bin/true
+  net: host
   environment:
     - FOO=1
     - BAR=1

+ 1 - 0
tests/fixtures/extends/docker-compose.yml

@@ -11,6 +11,7 @@ myweb:
     BAR: "2"
     # add BAZ
     BAZ: "2"
+  net: bridge
 mydb:
   image: busybox
   command: top

+ 12 - 0
tests/fixtures/extends/invalid-net-v2.yml

@@ -0,0 +1,12 @@
+version: 2
+services:
+  myweb:
+    build: '.'
+    extends:
+      service: web
+    command: top
+  web:
+    build: '.'
+    network_mode: "service:net"
+  net:
+    build: '.'

+ 9 - 0
tests/fixtures/networks/bridge.yml

@@ -0,0 +1,9 @@
+version: 2
+
+services:
+  web:
+    image: busybox
+    command: top
+    networks:
+      - bridge
+      - default

+ 27 - 0
tests/fixtures/networks/network-mode.yml

@@ -0,0 +1,27 @@
+version: 2
+
+services:
+  bridge:
+    image: busybox
+    command: top
+    network_mode: bridge
+
+  service:
+    image: busybox
+    command: top
+    network_mode: "service:bridge"
+
+  container:
+    image: busybox
+    command: top
+    network_mode: "container:composetest_network_mode_container"
+
+  host:
+    image: busybox
+    command: top
+    network_mode: host
+
+  none:
+    image: busybox
+    command: top
+    network_mode: none

+ 0 - 17
tests/fixtures/networks/predefined-networks.yml

@@ -1,17 +0,0 @@
-version: 2
-
-services:
-  bridge:
-    image: busybox
-    command: top
-    networks: ["bridge"]
-
-  host:
-    image: busybox
-    command: top
-    networks: ["host"]
-
-  none:
-    image: busybox
-    command: top
-    networks: []

+ 87 - 14
tests/integration/project_test.py

@@ -4,10 +4,12 @@ from __future__ import unicode_literals
 import random
 
 import py
+import pytest
 from docker.errors import NotFound
 
 from .testcases import DockerClientTestCase
 from compose.config import config
+from compose.config import ConfigurationError
 from compose.config.types import VolumeFromSpec
 from compose.config.types import VolumeSpec
 from compose.const import LABEL_PROJECT
@@ -104,30 +106,55 @@ class ProjectTest(DockerClientTestCase):
         db = project.get_service('db')
         self.assertEqual(db._get_volumes_from(), [data_container.id + ':rw'])
 
-    def test_net_from_service(self):
+    @v2_only()
+    def test_network_mode_from_service(self):
         project = Project.from_config(
             name='composetest',
+            client=self.client,
             config_data=build_service_dicts({
-                'net': {
-                    'image': 'busybox:latest',
-                    'command': ["top"]
-                },
-                'web': {
-                    'image': 'busybox:latest',
-                    'net': 'container:net',
-                    'command': ["top"]
+                'version': 2,
+                'services': {
+                    'net': {
+                        'image': 'busybox:latest',
+                        'command': ["top"]
+                    },
+                    'web': {
+                        'image': 'busybox:latest',
+                        'network_mode': 'service:net',
+                        'command': ["top"]
+                    },
                 },
             }),
-            client=self.client,
         )
 
         project.up()
 
         web = project.get_service('web')
         net = project.get_service('net')
-        self.assertEqual(web.net.mode, 'container:' + net.containers()[0].id)
+        self.assertEqual(web.network_mode.mode, 'container:' + net.containers()[0].id)
+
+    @v2_only()
+    def test_network_mode_from_container(self):
+        def get_project():
+            return Project.from_config(
+                name='composetest',
+                config_data=build_service_dicts({
+                    'version': 2,
+                    'services': {
+                        'web': {
+                            'image': 'busybox:latest',
+                            'network_mode': 'container:composetest_net_container'
+                        },
+                    },
+                }),
+                client=self.client,
+            )
+
+        with pytest.raises(ConfigurationError) as excinfo:
+            get_project()
+
+        assert "container 'composetest_net_container' which does not exist" in excinfo.exconly()
 
-    def test_net_from_container(self):
         net_container = Container.create(
             self.client,
             image='busybox:latest',
@@ -137,12 +164,24 @@ class ProjectTest(DockerClientTestCase):
         )
         net_container.start()
 
+        project = get_project()
+        project.up()
+
+        web = project.get_service('web')
+        self.assertEqual(web.network_mode.mode, 'container:' + net_container.id)
+
+    def test_net_from_service_v1(self):
         project = Project.from_config(
             name='composetest',
             config_data=build_service_dicts({
+                'net': {
+                    'image': 'busybox:latest',
+                    'command': ["top"]
+                },
                 'web': {
                     'image': 'busybox:latest',
-                    'net': 'container:composetest_net_container'
+                    'net': 'container:net',
+                    'command': ["top"]
                 },
             }),
             client=self.client,
@@ -151,7 +190,41 @@ class ProjectTest(DockerClientTestCase):
         project.up()
 
         web = project.get_service('web')
-        self.assertEqual(web.net.mode, 'container:' + net_container.id)
+        net = project.get_service('net')
+        self.assertEqual(web.network_mode.mode, 'container:' + net.containers()[0].id)
+
+    def test_net_from_container_v1(self):
+        def get_project():
+            return Project.from_config(
+                name='composetest',
+                config_data=build_service_dicts({
+                    'web': {
+                        'image': 'busybox:latest',
+                        'net': 'container:composetest_net_container'
+                    },
+                }),
+                client=self.client,
+            )
+
+        with pytest.raises(ConfigurationError) as excinfo:
+            get_project()
+
+        assert "container 'composetest_net_container' which does not exist" in excinfo.exconly()
+
+        net_container = Container.create(
+            self.client,
+            image='busybox:latest',
+            name='composetest_net_container',
+            command='top',
+            labels={LABEL_PROJECT: 'composetest'},
+        )
+        net_container.start()
+
+        project = get_project()
+        project.up()
+
+        web = project.get_service('web')
+        self.assertEqual(web.network_mode.mode, 'container:' + net_container.id)
 
     def test_start_pause_unpause_stop_kill_remove(self):
         web = self.create_service('web')

+ 4 - 4
tests/integration/service_test.py

@@ -26,7 +26,7 @@ from compose.const import LABEL_VERSION
 from compose.container import Container
 from compose.service import ConvergencePlan
 from compose.service import ConvergenceStrategy
-from compose.service import Net
+from compose.service import NetworkMode
 from compose.service import Service
 
 
@@ -752,17 +752,17 @@ class ServiceTest(DockerClientTestCase):
         assert len(service.containers(stopped=True)) == 2
 
     def test_network_mode_none(self):
-        service = self.create_service('web', net=Net('none'))
+        service = self.create_service('web', network_mode=NetworkMode('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=Net('bridge'))
+        service = self.create_service('web', network_mode=NetworkMode('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=Net('host'))
+        service = self.create_service('web', network_mode=NetworkMode('host'))
         container = create_and_start_container(service)
         self.assertEqual(container.get('HostConfig.NetworkMode'), 'host')
 

+ 132 - 2
tests/unit/config/config_test.py

@@ -1015,6 +1015,126 @@ class ConfigTest(unittest.TestCase):
         assert "Service 'one' depends on service 'three'" in exc.exconly()
 
 
+class NetworkModeTest(unittest.TestCase):
+    def test_network_mode_standard(self):
+        config_data = config.load(build_config_details({
+            'version': 2,
+            'services': {
+                'web': {
+                    'image': 'busybox',
+                    'command': "top",
+                    'network_mode': 'bridge',
+                },
+            },
+        }))
+
+        assert config_data.services[0]['network_mode'] == 'bridge'
+
+    def test_network_mode_standard_v1(self):
+        config_data = config.load(build_config_details({
+            'web': {
+                'image': 'busybox',
+                'command': "top",
+                'net': 'bridge',
+            },
+        }))
+
+        assert config_data.services[0]['network_mode'] == 'bridge'
+        assert 'net' not in config_data.services[0]
+
+    def test_network_mode_container(self):
+        config_data = config.load(build_config_details({
+            'version': 2,
+            'services': {
+                'web': {
+                    'image': 'busybox',
+                    'command': "top",
+                    'network_mode': 'container:foo',
+                },
+            },
+        }))
+
+        assert config_data.services[0]['network_mode'] == 'container:foo'
+
+    def test_network_mode_container_v1(self):
+        config_data = config.load(build_config_details({
+            'web': {
+                'image': 'busybox',
+                'command': "top",
+                'net': 'container:foo',
+            },
+        }))
+
+        assert config_data.services[0]['network_mode'] == 'container:foo'
+
+    def test_network_mode_service(self):
+        config_data = config.load(build_config_details({
+            'version': 2,
+            'services': {
+                'web': {
+                    'image': 'busybox',
+                    'command': "top",
+                    'network_mode': 'service:foo',
+                },
+                'foo': {
+                    'image': 'busybox',
+                    'command': "top",
+                },
+            },
+        }))
+
+        assert config_data.services[1]['network_mode'] == 'service:foo'
+
+    def test_network_mode_service_v1(self):
+        config_data = config.load(build_config_details({
+            'web': {
+                'image': 'busybox',
+                'command': "top",
+                'net': 'container:foo',
+            },
+            'foo': {
+                'image': 'busybox',
+                'command': "top",
+            },
+        }))
+
+        assert config_data.services[1]['network_mode'] == 'service:foo'
+
+    def test_network_mode_service_nonexistent(self):
+        with pytest.raises(ConfigurationError) as excinfo:
+            config.load(build_config_details({
+                'version': 2,
+                'services': {
+                    'web': {
+                        'image': 'busybox',
+                        'command': "top",
+                        'network_mode': 'service:foo',
+                    },
+                },
+            }))
+
+        assert "service 'foo' which is undefined" in excinfo.exconly()
+
+    def test_network_mode_plus_networks_is_invalid(self):
+        with pytest.raises(ConfigurationError) as excinfo:
+            config.load(build_config_details({
+                'version': 2,
+                'services': {
+                    'web': {
+                        'image': 'busybox',
+                        'command': "top",
+                        'network_mode': 'bridge',
+                        'networks': ['front'],
+                    },
+                },
+                'networks': {
+                    'front': None,
+                }
+            }))
+
+        assert "'network_mode' and 'networks' cannot be combined" in excinfo.exconly()
+
+
 class PortsTest(unittest.TestCase):
     INVALID_PORTS_TYPES = [
         {"1": "8000"},
@@ -1642,6 +1762,7 @@ class ExtendsTest(unittest.TestCase):
                 'name': 'myweb',
                 'image': 'busybox',
                 'command': 'top',
+                'network_mode': 'bridge',
                 'links': ['mydb:db'],
                 'environment': {
                     "FOO": "1",
@@ -1659,6 +1780,7 @@ class ExtendsTest(unittest.TestCase):
                 'name': 'web',
                 'image': 'busybox',
                 'command': '/bin/true',
+                'network_mode': 'host',
                 'environment': {
                     "FOO": "2",
                     "BAR": "1",
@@ -1677,6 +1799,7 @@ class ExtendsTest(unittest.TestCase):
                 'name': 'myweb',
                 'image': 'busybox',
                 'command': '/bin/true',
+                'network_mode': 'host',
                 'environment': {
                     "FOO": "2",
                     "BAR": "2",
@@ -1867,11 +1990,18 @@ class ExtendsTest(unittest.TestCase):
             load_from_filename('tests/fixtures/extends/invalid-volumes.yml')
 
     def test_invalid_net_in_extended_service(self):
-        expected_error_msg = "services with 'net: container' cannot be extended"
+        with pytest.raises(ConfigurationError) as excinfo:
+            load_from_filename('tests/fixtures/extends/invalid-net-v2.yml')
 
-        with self.assertRaisesRegexp(ConfigurationError, expected_error_msg):
+        assert 'network_mode: service' in excinfo.exconly()
+        assert 'cannot be extended' in excinfo.exconly()
+
+        with pytest.raises(ConfigurationError) as excinfo:
             load_from_filename('tests/fixtures/extends/invalid-net.yml')
 
+        assert 'net: container' in excinfo.exconly()
+        assert 'cannot be extended' in excinfo.exconly()
+
     @mock.patch.dict(os.environ)
     def test_load_config_runs_interpolation_in_extended_service(self):
         os.environ.update(HOSTNAME_VALUE="penguin")

+ 2 - 2
tests/unit/config/sort_services_test.py

@@ -100,7 +100,7 @@ class TestSortService(object):
             },
             {
                 'name': 'parent',
-                'net': 'container:child'
+                'network_mode': 'service:child'
             },
             {
                 'name': 'child'
@@ -137,7 +137,7 @@ class TestSortService(object):
     def test_sort_service_dicts_7(self):
         services = [
             {
-                'net': 'container:three',
+                'network_mode': 'service:three',
                 'name': 'four'
             },
             {

+ 5 - 5
tests/unit/project_test.py

@@ -349,7 +349,7 @@ class ProjectTest(unittest.TestCase):
             ),
         )
         service = project.get_service('test')
-        self.assertEqual(service.net.id, None)
+        self.assertEqual(service.network_mode.id, None)
         self.assertNotIn('NetworkMode', service._get_container_host_config({}))
 
     def test_use_net_from_container(self):
@@ -365,7 +365,7 @@ class ProjectTest(unittest.TestCase):
                     {
                         'name': 'test',
                         'image': 'busybox:latest',
-                        'net': 'container:aaa'
+                        'network_mode': 'container:aaa'
                     },
                 ],
                 networks=None,
@@ -373,7 +373,7 @@ class ProjectTest(unittest.TestCase):
             ),
         )
         service = project.get_service('test')
-        self.assertEqual(service.net.mode, 'container:' + container_id)
+        self.assertEqual(service.network_mode.mode, 'container:' + container_id)
 
     def test_use_net_from_service(self):
         container_name = 'test_aaa_1'
@@ -398,7 +398,7 @@ class ProjectTest(unittest.TestCase):
                     {
                         'name': 'test',
                         'image': 'busybox:latest',
-                        'net': 'container:aaa'
+                        'network_mode': 'service:aaa'
                     },
                 ],
                 networks=None,
@@ -407,7 +407,7 @@ class ProjectTest(unittest.TestCase):
         )
 
         service = project.get_service('test')
-        self.assertEqual(service.net.mode, 'container:' + container_name)
+        self.assertEqual(service.network_mode.mode, 'container:' + container_name)
 
     def test_uses_default_network_true(self):
         project = Project.from_config(

+ 26 - 26
tests/unit/service_test.py

@@ -15,16 +15,16 @@ from compose.const import LABEL_SERVICE
 from compose.container import Container
 from compose.service import build_ulimits
 from compose.service import build_volume_binding
-from compose.service import ContainerNet
+from compose.service import ContainerNetworkMode
 from compose.service import get_container_data_volumes
 from compose.service import ImageType
 from compose.service import merge_volume_bindings
 from compose.service import NeedsBuildError
-from compose.service import Net
+from compose.service import NetworkMode
 from compose.service import NoSuchImageError
 from compose.service import parse_repository_tag
 from compose.service import Service
-from compose.service import ServiceNet
+from compose.service import ServiceNetworkMode
 from compose.service import warn_on_masked_volume
 
 
@@ -407,7 +407,7 @@ class ServiceTest(unittest.TestCase):
             'foo',
             image='example.com/foo',
             client=self.mock_client,
-            net=ServiceNet(Service('other')),
+            network_mode=ServiceNetworkMode(Service('other')),
             links=[(Service('one'), 'one')],
             volumes_from=[VolumeFromSpec(Service('two'), 'rw', 'service')])
 
@@ -421,7 +421,7 @@ class ServiceTest(unittest.TestCase):
         }
         self.assertEqual(config_dict, expected)
 
-    def test_config_dict_with_net_from_container(self):
+    def test_config_dict_with_network_mode_from_container(self):
         self.mock_client.inspect_image.return_value = {'Id': 'abcd'}
         container = Container(
             self.mock_client,
@@ -430,7 +430,7 @@ class ServiceTest(unittest.TestCase):
             'foo',
             image='example.com/foo',
             client=self.mock_client,
-            net=container)
+            network_mode=ContainerNetworkMode(container))
 
         config_dict = service.config_dict()
         expected = {
@@ -589,20 +589,20 @@ class BuildUlimitsTestCase(unittest.TestCase):
 
 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_network_mode(self):
+        network_mode = NetworkMode('host')
+        self.assertEqual(network_mode.id, 'host')
+        self.assertEqual(network_mode.mode, 'host')
+        self.assertEqual(network_mode.service_name, None)
 
-    def test_net_container(self):
+    def test_network_mode_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)
+        network_mode = ContainerNetworkMode(Container(None, {'Id': container_id}))
+        self.assertEqual(network_mode.id, container_id)
+        self.assertEqual(network_mode.mode, 'container:' + container_id)
+        self.assertEqual(network_mode.service_name, None)
 
-    def test_net_service(self):
+    def test_network_mode_service(self):
         container_id = 'bbbb'
         service_name = 'web'
         mock_client = mock.create_autospec(docker.Client)
@@ -611,23 +611,23 @@ class NetTestCase(unittest.TestCase):
         ]
 
         service = Service(name=service_name, client=mock_client)
-        net = ServiceNet(service)
+        network_mode = ServiceNetworkMode(service)
 
-        self.assertEqual(net.id, service_name)
-        self.assertEqual(net.mode, 'container:' + container_id)
-        self.assertEqual(net.service_name, service_name)
+        self.assertEqual(network_mode.id, service_name)
+        self.assertEqual(network_mode.mode, 'container:' + container_id)
+        self.assertEqual(network_mode.service_name, service_name)
 
-    def test_net_service_no_containers(self):
+    def test_network_mode_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)
-        net = ServiceNet(service)
+        network_mode = ServiceNetworkMode(service)
 
-        self.assertEqual(net.id, service_name)
-        self.assertEqual(net.mode, None)
-        self.assertEqual(net.service_name, service_name)
+        self.assertEqual(network_mode.id, service_name)
+        self.assertEqual(network_mode.mode, None)
+        self.assertEqual(network_mode.service_name, service_name)
 
 
 def build_mount(destination, source, mode='rw'):