Bladeren bron

Specify networks in Compose file

There's not yet a proper way for services to join networks

Signed-off-by: Aanand Prasad <[email protected]>
Aanand Prasad 9 jaren geleden
bovenliggende
commit
69ed5f9c48

+ 1 - 2
compose/cli/main.py

@@ -696,8 +696,7 @@ def run_one_off_container(container_options, project, service, options):
                 start_deps=True,
                 strategy=ConvergenceStrategy.never)
 
-    if project.use_networking:
-        project.ensure_network_exists()
+    project.initialize_networks()
 
     container = service.create_container(
         quiet=True,

+ 25 - 18
compose/config/config.py

@@ -139,14 +139,16 @@ class ConfigFile(namedtuple('_ConfigFile', 'filename config')):
         return {} if self.version == 1 else self.config.get('volumes', {})
 
 
-class Config(namedtuple('_Config', 'version services volumes')):
+class Config(namedtuple('_Config', 'version services volumes networks')):
     """
     :param version: configuration version
     :type  version: int
     :param services: List of service description dictionaries
     :type  services: :class:`list`
-    :param volumes: List of volume description dictionaries
-    :type  volumes: :class:`list`
+    :param volumes: Dictionary mapping volume names to description dictionaries
+    :type  volumes: :class:`dict`
+    :param networks: Dictionary mapping network names to description dictionaries
+    :type  networks: :class:`dict`
     """
 
 
@@ -256,39 +258,44 @@ def load(config_details):
     config_details = config_details._replace(config_files=processed_files)
 
     main_file = config_details.config_files[0]
-    volumes = load_volumes(config_details.config_files)
+    volumes = load_mapping(config_details.config_files, 'volumes', 'Volume')
+    networks = load_mapping(config_details.config_files, 'networks', 'Network')
     service_dicts = load_services(
         config_details.working_dir,
         main_file.filename,
         [file.get_service_dicts() for file in config_details.config_files],
         main_file.version)
-    return Config(main_file.version, service_dicts, volumes)
+    return Config(main_file.version, service_dicts, volumes, networks)
 
 
-def load_volumes(config_files):
-    volumes = {}
+def load_mapping(config_files, key, entity_type):
+    mapping = {}
+
     for config_file in config_files:
-        for name, volume_config in config_file.get_volumes().items():
-            volumes[name] = volume_config or {}
-            if not volume_config:
+        for name, config in config_file.config.get(key, {}).items():
+            mapping[name] = config or {}
+            if not config:
                 continue
 
-            external = volume_config.get('external')
+            external = config.get('external')
             if external:
-                if len(volume_config.keys()) > 1:
+                if len(config.keys()) > 1:
                     raise ConfigurationError(
-                        'Volume {0} declared as external but specifies'
-                        ' additional attributes ({1}). '.format(
+                        '{} {} declared as external but specifies'
+                        ' additional attributes ({}). '.format(
+                            entity_type,
                             name,
-                            ', '.join([k for k in volume_config.keys() if k != 'external'])
+                            ', '.join([k for k in config.keys() if k != 'external'])
                         )
                     )
                 if isinstance(external, dict):
-                    volume_config['external_name'] = external.get('name')
+                    config['external_name'] = external.get('name')
                 else:
-                    volume_config['external_name'] = name
+                    config['external_name'] = name
+
+            mapping[name] = config
 
-    return volumes
+    return mapping
 
 
 def load_services(working_dir, filename, service_configs, version):

+ 13 - 0
compose/config/fields_schema_v2.json

@@ -17,6 +17,15 @@
       },
       "additionalProperties": false
     },
+    "networks": {
+      "id": "#/properties/networks",
+      "type": "object",
+      "patternProperties": {
+        "^[a-zA-Z0-9._-]+$": {
+          "$ref": "#/definitions/network"
+        }
+      }
+    },
     "volumes": {
       "id": "#/properties/volumes",
       "type": "object",
@@ -30,6 +39,10 @@
   },
 
   "definitions": {
+    "network": {
+      "id": "#/definitions/network",
+      "type": "object"
+    },
     "volume": {
       "id": "#/definitions/volume",
       "type": ["object", "null"],

+ 57 - 0
compose/network.py

@@ -0,0 +1,57 @@
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+import logging
+
+from docker.errors import NotFound
+
+from .config import ConfigurationError
+
+
+log = logging.getLogger(__name__)
+
+
+class Network(object):
+    def __init__(self, client, project, name, driver=None, driver_opts=None):
+        self.client = client
+        self.project = project
+        self.name = name
+        self.driver = driver
+        self.driver_opts = driver_opts
+
+    def ensure(self):
+        try:
+            data = self.inspect()
+            if self.driver and data['Driver'] != self.driver:
+                raise ConfigurationError(
+                    'Network {} needs to be recreated - driver has changed'
+                    .format(self.full_name))
+            if data['Options'] != (self.driver_opts or {}):
+                raise ConfigurationError(
+                    'Network {} needs to be recreated - options have changed'
+                    .format(self.full_name))
+        except NotFound:
+            driver_name = 'the default driver'
+            if self.driver:
+                driver_name = 'driver "{}"'.format(self.driver)
+
+            log.info(
+                'Creating network "{}" with {}'
+                .format(self.full_name, driver_name)
+            )
+
+            self.client.create_network(
+                self.full_name, self.driver, self.driver_opts
+            )
+
+    def remove(self):
+        # TODO: don't remove external networks
+        log.info("Removing network {}".format(self.full_name))
+        self.client.remove_network(self.full_name)
+
+    def inspect(self):
+        return self.client.inspect_network(self.full_name)
+
+    @property
+    def full_name(self):
+        return '{0}_{1}'.format(self.project, self.name)

+ 52 - 75
compose/project.py

@@ -17,6 +17,7 @@ from .const import LABEL_ONE_OFF
 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 ConvergenceStrategy
 from .service import Net
@@ -33,12 +34,14 @@ class Project(object):
     """
     A collection of services.
     """
-    def __init__(self, name, services, client, volumes=None, use_networking=False, network_driver=None):
+    def __init__(self, name, services, client, networks=None, volumes=None,
+                 use_networking=False, network_driver=None):
         self.name = name
         self.services = services
         self.client = client
         self.use_networking = use_networking
         self.network_driver = network_driver
+        self.networks = networks or []
         self.volumes = volumes or []
 
     def labels(self, one_off=False):
@@ -55,9 +58,6 @@ class Project(object):
         use_networking = (config_data.version and config_data.version >= 2)
         project = cls(name, [], client, use_networking=use_networking)
 
-        if use_networking:
-            remove_links(config_data.services)
-
         for service_dict in config_data.services:
             links = project.get_links(service_dict)
             volumes_from = get_volumes_from(project, service_dict)
@@ -72,6 +72,16 @@ class Project(object):
                     net=net,
                     volumes_from=volumes_from,
                     **service_dict))
+
+        if config_data.networks:
+            for network_name, data in config_data.networks.items():
+                project.networks.append(
+                    Network(
+                        client=client, project=name, name=network_name,
+                        driver=data.get('driver'), driver_opts=data.get('driver_opts')
+                    )
+                )
+
         if config_data.volumes:
             for vol_name, data in config_data.volumes.items():
                 project.volumes.append(
@@ -82,6 +92,7 @@ class Project(object):
                         external_name=data.get('external_name')
                     )
                 )
+
         return project
 
     @property
@@ -124,20 +135,18 @@ class Project(object):
         Raises NoSuchService if any of the named services do not exist.
         """
         if service_names is None or len(service_names) == 0:
-            return self.get_services(
-                service_names=self.service_names,
-                include_deps=include_deps
-            )
-        else:
-            unsorted = [self.get_service(name) for name in service_names]
-            services = [s for s in self.services if s in unsorted]
+            service_names = self.service_names
+
+        unsorted = [self.get_service(name) for name in service_names]
+        services = [s for s in self.services if s in unsorted]
 
-            if include_deps:
-                services = reduce(self._inject_deps, services, [])
+        if include_deps:
+            services = reduce(self._inject_deps, services, [])
 
-            uniques = []
-            [uniques.append(s) for s in services if s not in uniques]
-            return uniques
+        uniques = []
+        [uniques.append(s) for s in services if s not in uniques]
+
+        return uniques
 
     def get_services_without_duplicate(self, service_names=None, include_deps=False):
         services = self.get_services(service_names, include_deps)
@@ -166,7 +175,7 @@ class Project(object):
         net = service_dict.pop('net', None)
         if not net:
             if self.use_networking:
-                return Net(self.default_network_name)
+                return Net(self.default_network.full_name)
             return Net(None)
 
         net_name = get_service_name_from_net(net)
@@ -251,7 +260,7 @@ class Project(object):
     def down(self, remove_image_type, include_volumes):
         self.stop()
         self.remove_stopped(v=include_volumes)
-        self.remove_network()
+        self.remove_default_network()
 
         if include_volumes:
             self.remove_volumes()
@@ -262,10 +271,34 @@ class Project(object):
         for service in self.get_services():
             service.remove_image(remove_image_type)
 
+    def remove_default_network(self):
+        if not self.use_networking:
+            return
+        if self.uses_default_network():
+            self.default_network.remove()
+
     def remove_volumes(self):
         for volume in self.volumes:
             volume.remove()
 
+    def initialize_networks(self):
+        networks = self.networks
+        if self.uses_default_network():
+            networks.append(self.default_network)
+
+        for network in networks:
+            network.ensure()
+
+    def uses_default_network(self):
+        return any(
+            service.net.mode == self.default_network.full_name
+            for service in self.services
+        )
+
+    @property
+    def default_network(self):
+        return Network(client=self.client, project=self.name, name='default')
+
     def restart(self, service_names=None, **options):
         containers = self.containers(service_names, stopped=True)
         parallel.parallel_restart(containers, options)
@@ -335,9 +368,7 @@ class Project(object):
 
         plans = self._get_convergence_plans(services, strategy)
 
-        if self.use_networking and self.uses_default_network():
-            self.ensure_network_exists()
-
+        self.initialize_networks()
         self.initialize_volumes()
 
         return [
@@ -395,40 +426,6 @@ class Project(object):
 
         return [c for c in containers if matches_service_names(c)]
 
-    def get_network(self):
-        try:
-            return self.client.inspect_network(self.default_network_name)
-        except NotFound:
-            return None
-
-    def ensure_network_exists(self):
-        # TODO: recreate network if driver has changed?
-        if self.get_network() is None:
-            driver_name = 'the default driver'
-            if self.network_driver:
-                driver_name = 'driver "{}"'.format(self.network_driver)
-
-            log.info(
-                'Creating network "{}" with {}'
-                .format(self.default_network_name, driver_name)
-            )
-            self.client.create_network(self.default_network_name, driver=self.network_driver)
-
-    def remove_network(self):
-        if not self.use_networking:
-            return
-        network = self.get_network()
-        if network:
-            log.info("Removing network %s", self.default_network_name)
-            self.client.remove_network(network['Id'])
-
-    def uses_default_network(self):
-        return any(service.net.mode == self.default_network_name for service in self.services)
-
-    @property
-    def default_network_name(self):
-        return '{}_default'.format(self.name)
-
     def _inject_deps(self, acc, service):
         dep_names = service.get_dependency_names()
 
@@ -444,26 +441,6 @@ class Project(object):
         return acc + dep_services
 
 
-def remove_links(service_dicts):
-    services_with_links = [s for s in service_dicts if 'links' in s]
-    if not services_with_links:
-        return
-
-    if len(services_with_links) == 1:
-        prefix = '"{}" defines'.format(services_with_links[0]['name'])
-    else:
-        prefix = 'Some services ({}) define'.format(
-            ", ".join('"{}"'.format(s['name']) for s in services_with_links))
-
-    log.warn(
-        '\n{} links, which are not compatible with Docker networking and will be ignored.\n'
-        'Future versions of Docker will not support links - you should remove them for '
-        'forwards-compatibility.\n'.format(prefix))
-
-    for s in services_with_links:
-        del s['links']
-
-
 def get_volumes_from(project, service_dict):
     volumes_from = service_dict.pop('volumes_from', None)
     if not volumes_from:

+ 16 - 3
tests/acceptance/cli_test.py

@@ -354,7 +354,7 @@ class CLITestCase(DockerClientTestCase):
         self.base_dir = 'tests/fixtures/links-composefile'
         self.dispatch(['up', '-d'], None)
 
-        networks = self.client.networks(names=[self.project.default_network_name])
+        networks = self.client.networks(names=[self.project.default_network.full_name])
         self.assertEqual(len(networks), 0)
 
         for service in self.project.get_services():
@@ -371,7 +371,7 @@ class CLITestCase(DockerClientTestCase):
 
         services = self.project.get_services()
 
-        networks = self.client.networks(names=[self.project.default_network_name])
+        networks = self.client.networks(names=[self.project.default_network.full_name])
         for n in networks:
             self.addCleanup(self.client.remove_network, n['Id'])
         self.assertEqual(len(networks), 1)
@@ -388,6 +388,19 @@ class CLITestCase(DockerClientTestCase):
         web_container = self.project.get_service('simple').containers()[0]
         self.assertFalse(web_container.get('HostConfig.Links'))
 
+    def test_up_with_networks(self):
+        self.base_dir = 'tests/fixtures/networks'
+        self.dispatch(['up', '-d'], None)
+
+        networks = self.client.networks(names=[
+            '{}_{}'.format(self.project.name, n)
+            for n in ['foo', 'bar']])
+
+        self.assertEqual(len(networks), 2)
+
+        for net in networks:
+            self.assertEqual(net['Driver'], 'bridge')
+
     def test_up_with_links_is_invalid(self):
         self.base_dir = 'tests/fixtures/v2-simple'
 
@@ -698,7 +711,7 @@ class CLITestCase(DockerClientTestCase):
         self.dispatch(['run', 'simple', 'true'], None)
         service = self.project.get_service('simple')
         container, = service.containers(stopped=True, one_off=True)
-        networks = self.client.networks(names=[self.project.default_network_name])
+        networks = self.client.networks(names=[self.project.default_network.full_name])
         for n in networks:
             self.addCleanup(self.client.remove_network, n['Id'])
         self.assertEqual(len(networks), 1)

+ 7 - 0
tests/fixtures/networks/docker-compose.yml

@@ -0,0 +1,7 @@
+version: 2
+
+networks:
+  foo:
+    driver:
+
+  bar: {}

+ 9 - 0
tests/fixtures/no-links-composefile/docker-compose.yml

@@ -0,0 +1,9 @@
+db:
+  image: busybox:latest
+  command: top
+web:
+  image: busybox:latest
+  command: top
+console:
+  image: busybox:latest
+  command: top

+ 69 - 44
tests/integration/project_test.py

@@ -14,7 +14,6 @@ from compose.const import LABEL_PROJECT
 from compose.container import Container
 from compose.project import Project
 from compose.service import ConvergenceStrategy
-from compose.service import Net
 
 
 def build_service_dicts(service_config):
@@ -104,21 +103,6 @@ class ProjectTest(DockerClientTestCase):
         db = project.get_service('db')
         self.assertEqual(db._get_volumes_from(), [data_container.id + ':rw'])
 
-    def test_get_network_does_not_exist(self):
-        project = Project('composetest', [], self.client)
-        assert project.get_network() is None
-
-    def test_get_network(self):
-        project_name = 'network_does_exist'
-        network_name = '{}_default'.format(project_name)
-
-        project = Project(project_name, [], self.client)
-        self.client.create_network(network_name)
-        self.addCleanup(self.client.remove_network, network_name)
-
-        assert isinstance(project.get_network(), dict)
-        assert project.get_network()['Name'] == network_name
-
     def test_net_from_service(self):
         project = Project.from_config(
             name='composetest',
@@ -473,18 +457,6 @@ class ProjectTest(DockerClientTestCase):
         self.assertEqual(len(project.get_service('data').containers(stopped=True)), 1)
         self.assertEqual(len(project.get_service('console').containers()), 0)
 
-    def test_project_up_with_custom_network(self):
-        network_name = 'composetest-custom'
-
-        self.client.create_network(network_name)
-        self.addCleanup(self.client.remove_network, network_name)
-
-        web = self.create_service('web', net=Net(network_name))
-        project = Project('composetest', [web], self.client, use_networking=True)
-        project.up()
-
-        assert project.get_network() is None
-
     def test_unscale_after_restart(self):
         web = self.create_service('web')
         project = Project('composetest', [web], self.client)
@@ -510,15 +482,50 @@ class ProjectTest(DockerClientTestCase):
         service = project.get_service('web')
         self.assertEqual(len(service.containers()), 1)
 
+    def test_project_up_networks(self):
+        config_data = config.Config(
+            version=2,
+            services=[{
+                'name': 'web',
+                'image': 'busybox:latest',
+                'command': 'top',
+            }],
+            volumes={},
+            networks={
+                'foo': {'driver': 'bridge'},
+                'bar': {'driver': None},
+                'baz': {},
+            },
+        )
+
+        project = Project.from_config(
+            client=self.client,
+            name='composetest',
+            config_data=config_data,
+        )
+        project.up()
+        self.assertEqual(len(project.containers()), 1)
+
+        for net_name in ['foo', 'bar', 'baz']:
+            full_net_name = 'composetest_{}'.format(net_name)
+            network_data = self.client.inspect_network(full_net_name)
+            self.assertEqual(network_data['Name'], full_net_name)
+
+        foo_data = self.client.inspect_network('composetest_foo')
+        self.assertEqual(foo_data['Driver'], 'bridge')
+
     def test_project_up_volumes(self):
         vol_name = '{0:x}'.format(random.getrandbits(32))
         full_vol_name = 'composetest_{0}'.format(vol_name)
         config_data = config.Config(
-            version=2, services=[{
+            version=2,
+            services=[{
                 'name': 'web',
                 'image': 'busybox:latest',
                 'command': 'top'
-            }], volumes={vol_name: {'driver': 'local'}}
+            }],
+            volumes={vol_name: {'driver': 'local'}},
+            networks={},
         )
 
         project = Project.from_config(
@@ -587,11 +594,14 @@ class ProjectTest(DockerClientTestCase):
         vol_name = '{0:x}'.format(random.getrandbits(32))
         full_vol_name = 'composetest_{0}'.format(vol_name)
         config_data = config.Config(
-            version=2, services=[{
+            version=2,
+            services=[{
                 'name': 'web',
                 'image': 'busybox:latest',
                 'command': 'top'
-            }], volumes={vol_name: {}}
+            }],
+            volumes={vol_name: {}},
+            networks={},
         )
 
         project = Project.from_config(
@@ -608,11 +618,14 @@ class ProjectTest(DockerClientTestCase):
         vol_name = '{0:x}'.format(random.getrandbits(32))
         full_vol_name = 'composetest_{0}'.format(vol_name)
         config_data = config.Config(
-            version=2, services=[{
+            version=2,
+            services=[{
                 'name': 'web',
                 'image': 'busybox:latest',
                 'command': 'top'
-            }], volumes={vol_name: {}}
+            }],
+            volumes={vol_name: {}},
+            networks={},
         )
 
         project = Project.from_config(
@@ -629,11 +642,14 @@ class ProjectTest(DockerClientTestCase):
         vol_name = '{0:x}'.format(random.getrandbits(32))
 
         config_data = config.Config(
-            version=2, services=[{
+            version=2,
+            services=[{
                 'name': 'web',
                 'image': 'busybox:latest',
                 'command': 'top'
-            }], volumes={vol_name: {'driver': 'foobar'}}
+            }],
+            volumes={vol_name: {'driver': 'foobar'}},
+            networks={},
         )
 
         project = Project.from_config(
@@ -648,11 +664,14 @@ class ProjectTest(DockerClientTestCase):
         full_vol_name = 'composetest_{0}'.format(vol_name)
 
         config_data = config.Config(
-            version=2, services=[{
+            version=2,
+            services=[{
                 'name': 'web',
                 'image': 'busybox:latest',
                 'command': 'top'
-            }], volumes={vol_name: {'driver': 'local'}}
+            }],
+            volumes={vol_name: {'driver': 'local'}},
+            networks={},
         )
         project = Project.from_config(
             name='composetest',
@@ -683,13 +702,16 @@ class ProjectTest(DockerClientTestCase):
         full_vol_name = 'composetest_{0}'.format(vol_name)
         self.client.create_volume(vol_name)
         config_data = config.Config(
-            version=2, services=[{
+            version=2,
+            services=[{
                 'name': 'web',
                 'image': 'busybox:latest',
                 'command': 'top'
-            }], volumes={
+            }],
+            volumes={
                 vol_name: {'external': True, 'external_name': vol_name}
-            }
+            },
+            networks=None,
         )
         project = Project.from_config(
             name='composetest',
@@ -704,13 +726,16 @@ class ProjectTest(DockerClientTestCase):
         vol_name = '{0:x}'.format(random.getrandbits(32))
 
         config_data = config.Config(
-            version=2, services=[{
+            version=2,
+            services=[{
                 'name': 'web',
                 'image': 'busybox:latest',
                 'command': 'top'
-            }], volumes={
+            }],
+            volumes={
                 vol_name: {'external': True, 'external_name': vol_name}
-            }
+            },
+            networks=None,
         )
         project = Project.from_config(
             name='composetest',

+ 151 - 97
tests/unit/project_test.py

@@ -21,35 +21,27 @@ class ProjectTest(unittest.TestCase):
     def setUp(self):
         self.mock_client = mock.create_autospec(docker.Client)
 
-    def test_from_dict(self):
-        project = Project.from_config('composetest', Config(None, [
-            {
-                'name': 'web',
-                'image': 'busybox:latest'
-            },
-            {
-                'name': 'db',
-                'image': 'busybox:latest'
-            },
-        ], None), None)
-        self.assertEqual(len(project.services), 2)
-        self.assertEqual(project.get_service('web').name, 'web')
-        self.assertEqual(project.get_service('web').options['image'], 'busybox:latest')
-        self.assertEqual(project.get_service('db').name, 'db')
-        self.assertEqual(project.get_service('db').options['image'], 'busybox:latest')
-
     def test_from_config(self):
-        config = Config(None, [
-            {
-                'name': 'web',
-                'image': 'busybox:latest',
-            },
-            {
-                'name': 'db',
-                'image': 'busybox:latest',
-            },
-        ], None)
-        project = Project.from_config('composetest', config, None)
+        config = Config(
+            version=None,
+            services=[
+                {
+                    'name': 'web',
+                    'image': 'busybox:latest',
+                },
+                {
+                    'name': 'db',
+                    'image': 'busybox:latest',
+                },
+            ],
+            networks=None,
+            volumes=None,
+        )
+        project = Project.from_config(
+            name='composetest',
+            config_data=config,
+            client=None,
+        )
         self.assertEqual(len(project.services), 2)
         self.assertEqual(project.get_service('web').name, 'web')
         self.assertEqual(project.get_service('web').options['image'], 'busybox:latest')
@@ -58,16 +50,21 @@ class ProjectTest(unittest.TestCase):
         self.assertFalse(project.use_networking)
 
     def test_from_config_v2(self):
-        config = Config(2, [
-            {
-                'name': 'web',
-                'image': 'busybox:latest',
-            },
-            {
-                'name': 'db',
-                'image': 'busybox:latest',
-            },
-        ], None)
+        config = Config(
+            version=2,
+            services=[
+                {
+                    'name': 'web',
+                    'image': 'busybox:latest',
+                },
+                {
+                    'name': 'db',
+                    'image': 'busybox:latest',
+                },
+            ],
+            networks=None,
+            volumes=None,
+        )
         project = Project.from_config('composetest', config, None)
         self.assertEqual(len(project.services), 2)
         self.assertTrue(project.use_networking)
@@ -161,13 +158,20 @@ class ProjectTest(unittest.TestCase):
         container_id = 'aabbccddee'
         container_dict = dict(Name='aaa', Id=container_id)
         self.mock_client.inspect_container.return_value = container_dict
-        project = Project.from_config('test', Config(None, [
-            {
-                'name': 'test',
-                'image': 'busybox:latest',
-                'volumes_from': [VolumeFromSpec('aaa', 'rw', 'container')]
-            }
-        ], None), self.mock_client)
+        project = Project.from_config(
+            name='test',
+            client=self.mock_client,
+            config_data=Config(
+                version=None,
+                services=[{
+                    'name': 'test',
+                    'image': 'busybox:latest',
+                    'volumes_from': [VolumeFromSpec('aaa', 'rw', 'container')]
+                }],
+                networks=None,
+                volumes=None,
+            ),
+        )
         assert project.get_service('test')._get_volumes_from() == [container_id + ":rw"]
 
     def test_use_volumes_from_service_no_container(self):
@@ -180,33 +184,51 @@ class ProjectTest(unittest.TestCase):
                 "Image": 'busybox:latest'
             }
         ]
-        project = Project.from_config('test', Config(None, [
-            {
-                'name': 'vol',
-                'image': 'busybox:latest'
-            },
-            {
-                'name': 'test',
-                'image': 'busybox:latest',
-                'volumes_from': [VolumeFromSpec('vol', 'rw', 'service')]
-            }
-        ], None), self.mock_client)
+        project = Project.from_config(
+            name='test',
+            client=self.mock_client,
+            config_data=Config(
+                version=None,
+                services=[
+                    {
+                        'name': 'vol',
+                        'image': 'busybox:latest'
+                    },
+                    {
+                        'name': 'test',
+                        'image': 'busybox:latest',
+                        'volumes_from': [VolumeFromSpec('vol', 'rw', 'service')]
+                    }
+                ],
+                networks=None,
+                volumes=None,
+            ),
+        )
         assert project.get_service('test')._get_volumes_from() == [container_name + ":rw"]
 
     def test_use_volumes_from_service_container(self):
         container_ids = ['aabbccddee', '12345']
 
-        project = Project.from_config('test', Config(None, [
-            {
-                'name': 'vol',
-                'image': 'busybox:latest'
-            },
-            {
-                'name': 'test',
-                'image': 'busybox:latest',
-                'volumes_from': [VolumeFromSpec('vol', 'rw', 'service')]
-            }
-        ], None), None)
+        project = Project.from_config(
+            name='test',
+            client=None,
+            config_data=Config(
+                version=None,
+                services=[
+                    {
+                        'name': 'vol',
+                        'image': 'busybox:latest'
+                    },
+                    {
+                        'name': 'test',
+                        'image': 'busybox:latest',
+                        'volumes_from': [VolumeFromSpec('vol', 'rw', 'service')]
+                    }
+                ],
+                networks=None,
+                volumes=None,
+            ),
+        )
         with mock.patch.object(Service, 'containers') as mock_return:
             mock_return.return_value = [
                 mock.Mock(id=container_id, spec=Container)
@@ -313,12 +335,21 @@ class ProjectTest(unittest.TestCase):
         ]
 
     def test_net_unset(self):
-        project = Project.from_config('test', Config(None, [
-            {
-                'name': 'test',
-                'image': 'busybox:latest',
-            }
-        ], None), self.mock_client)
+        project = Project.from_config(
+            name='test',
+            client=self.mock_client,
+            config_data=Config(
+                version=None,
+                services=[
+                    {
+                        'name': 'test',
+                        'image': 'busybox:latest',
+                    }
+                ],
+                networks=None,
+                volumes=None,
+            ),
+        )
         service = project.get_service('test')
         self.assertEqual(service.net.id, None)
         self.assertNotIn('NetworkMode', service._get_container_host_config({}))
@@ -327,13 +358,22 @@ class ProjectTest(unittest.TestCase):
         container_id = 'aabbccddee'
         container_dict = dict(Name='aaa', Id=container_id)
         self.mock_client.inspect_container.return_value = container_dict
-        project = Project.from_config('test', Config(None, [
-            {
-                'name': 'test',
-                'image': 'busybox:latest',
-                'net': 'container:aaa'
-            }
-        ], None), self.mock_client)
+        project = Project.from_config(
+            name='test',
+            client=self.mock_client,
+            config_data=Config(
+                version=None,
+                services=[
+                    {
+                        'name': 'test',
+                        'image': 'busybox:latest',
+                        'net': 'container:aaa'
+                    },
+                ],
+                networks=None,
+                volumes=None,
+            ),
+        )
         service = project.get_service('test')
         self.assertEqual(service.net.mode, 'container:' + container_id)
 
@@ -347,17 +387,26 @@ class ProjectTest(unittest.TestCase):
                 "Image": 'busybox:latest'
             }
         ]
-        project = Project.from_config('test', Config(None, [
-            {
-                'name': 'aaa',
-                'image': 'busybox:latest'
-            },
-            {
-                'name': 'test',
-                'image': 'busybox:latest',
-                'net': 'container:aaa'
-            }
-        ], None), self.mock_client)
+        project = Project.from_config(
+            name='test',
+            client=self.mock_client,
+            config_data=Config(
+                version=None,
+                services=[
+                    {
+                        'name': 'aaa',
+                        'image': 'busybox:latest'
+                    },
+                    {
+                        'name': 'test',
+                        'image': 'busybox:latest',
+                        'net': 'container:aaa'
+                    },
+                ],
+                networks=None,
+                volumes=None,
+            ),
+        )
 
         service = project.get_service('test')
         self.assertEqual(service.net.mode, 'container:' + container_name)
@@ -403,11 +452,16 @@ class ProjectTest(unittest.TestCase):
             },
         }
         project = Project.from_config(
-            'test',
-            Config(None, [{
-                'name': 'web',
-                'image': 'busybox:latest',
-            }], None),
-            self.mock_client,
+            name='test',
+            client=self.mock_client,
+            config_data=Config(
+                version=None,
+                services=[{
+                    'name': 'web',
+                    'image': 'busybox:latest',
+                }],
+                networks=None,
+                volumes=None,
+            ),
         )
         self.assertEqual([c.id for c in project.containers()], ['1'])