Specify networks in Compose file

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

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
This commit is contained in:
Aanand Prasad 2015-12-17 17:49:48 +00:00
parent 8fca4f1628
commit 69ed5f9c48
10 changed files with 400 additions and 239 deletions

View File

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

View File

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

View File

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

57
compose/network.py Normal file
View File

@ -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)

View File

@ -17,6 +17,7 @@ from .const import LABEL_ONE_OFF
from .const import LABEL_PROJECT from .const import LABEL_PROJECT
from .const import LABEL_SERVICE from .const import LABEL_SERVICE
from .container import Container from .container import Container
from .network import Network
from .service import ContainerNet from .service import ContainerNet
from .service import ConvergenceStrategy from .service import ConvergenceStrategy
from .service import Net from .service import Net
@ -33,12 +34,14 @@ class Project(object):
""" """
A collection of services. 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.name = name
self.services = services self.services = services
self.client = client self.client = client
self.use_networking = use_networking self.use_networking = use_networking
self.network_driver = network_driver self.network_driver = network_driver
self.networks = networks or []
self.volumes = volumes or [] self.volumes = volumes or []
def labels(self, one_off=False): def labels(self, one_off=False):
@ -55,9 +58,6 @@ class Project(object):
use_networking = (config_data.version and config_data.version >= 2) use_networking = (config_data.version and config_data.version >= 2)
project = cls(name, [], client, use_networking=use_networking) project = cls(name, [], client, use_networking=use_networking)
if use_networking:
remove_links(config_data.services)
for service_dict in config_data.services: for service_dict in config_data.services:
links = project.get_links(service_dict) links = project.get_links(service_dict)
volumes_from = get_volumes_from(project, service_dict) volumes_from = get_volumes_from(project, service_dict)
@ -72,6 +72,16 @@ class Project(object):
net=net, net=net,
volumes_from=volumes_from, volumes_from=volumes_from,
**service_dict)) **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: if config_data.volumes:
for vol_name, data in config_data.volumes.items(): for vol_name, data in config_data.volumes.items():
project.volumes.append( project.volumes.append(
@ -82,6 +92,7 @@ class Project(object):
external_name=data.get('external_name') external_name=data.get('external_name')
) )
) )
return project return project
@property @property
@ -124,20 +135,18 @@ class Project(object):
Raises NoSuchService if any of the named services do not exist. Raises NoSuchService if any of the named services do not exist.
""" """
if service_names is None or len(service_names) == 0: if service_names is None or len(service_names) == 0:
return self.get_services( service_names = self.service_names
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]
if include_deps: unsorted = [self.get_service(name) for name in service_names]
services = reduce(self._inject_deps, services, []) services = [s for s in self.services if s in unsorted]
uniques = [] if include_deps:
[uniques.append(s) for s in services if s not in uniques] services = reduce(self._inject_deps, services, [])
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): def get_services_without_duplicate(self, service_names=None, include_deps=False):
services = self.get_services(service_names, include_deps) services = self.get_services(service_names, include_deps)
@ -166,7 +175,7 @@ class Project(object):
net = service_dict.pop('net', None) net = service_dict.pop('net', None)
if not net: if not net:
if self.use_networking: if self.use_networking:
return Net(self.default_network_name) return Net(self.default_network.full_name)
return Net(None) return Net(None)
net_name = get_service_name_from_net(net) net_name = get_service_name_from_net(net)
@ -251,7 +260,7 @@ class Project(object):
def down(self, remove_image_type, include_volumes): def down(self, remove_image_type, include_volumes):
self.stop() self.stop()
self.remove_stopped(v=include_volumes) self.remove_stopped(v=include_volumes)
self.remove_network() self.remove_default_network()
if include_volumes: if include_volumes:
self.remove_volumes() self.remove_volumes()
@ -262,10 +271,34 @@ class Project(object):
for service in self.get_services(): for service in self.get_services():
service.remove_image(remove_image_type) 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): def remove_volumes(self):
for volume in self.volumes: for volume in self.volumes:
volume.remove() 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): def restart(self, service_names=None, **options):
containers = self.containers(service_names, stopped=True) containers = self.containers(service_names, stopped=True)
parallel.parallel_restart(containers, options) parallel.parallel_restart(containers, options)
@ -335,9 +368,7 @@ class Project(object):
plans = self._get_convergence_plans(services, strategy) plans = self._get_convergence_plans(services, strategy)
if self.use_networking and self.uses_default_network(): self.initialize_networks()
self.ensure_network_exists()
self.initialize_volumes() self.initialize_volumes()
return [ return [
@ -395,40 +426,6 @@ class Project(object):
return [c for c in containers if matches_service_names(c)] 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): def _inject_deps(self, acc, service):
dep_names = service.get_dependency_names() dep_names = service.get_dependency_names()
@ -444,26 +441,6 @@ class Project(object):
return acc + dep_services 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): def get_volumes_from(project, service_dict):
volumes_from = service_dict.pop('volumes_from', None) volumes_from = service_dict.pop('volumes_from', None)
if not volumes_from: if not volumes_from:

View File

@ -354,7 +354,7 @@ class CLITestCase(DockerClientTestCase):
self.base_dir = 'tests/fixtures/links-composefile' self.base_dir = 'tests/fixtures/links-composefile'
self.dispatch(['up', '-d'], None) 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) self.assertEqual(len(networks), 0)
for service in self.project.get_services(): for service in self.project.get_services():
@ -371,7 +371,7 @@ class CLITestCase(DockerClientTestCase):
services = self.project.get_services() 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: for n in networks:
self.addCleanup(self.client.remove_network, n['Id']) self.addCleanup(self.client.remove_network, n['Id'])
self.assertEqual(len(networks), 1) self.assertEqual(len(networks), 1)
@ -388,6 +388,19 @@ class CLITestCase(DockerClientTestCase):
web_container = self.project.get_service('simple').containers()[0] web_container = self.project.get_service('simple').containers()[0]
self.assertFalse(web_container.get('HostConfig.Links')) 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): def test_up_with_links_is_invalid(self):
self.base_dir = 'tests/fixtures/v2-simple' self.base_dir = 'tests/fixtures/v2-simple'
@ -698,7 +711,7 @@ class CLITestCase(DockerClientTestCase):
self.dispatch(['run', 'simple', 'true'], None) self.dispatch(['run', 'simple', 'true'], None)
service = self.project.get_service('simple') service = self.project.get_service('simple')
container, = service.containers(stopped=True, one_off=True) 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: for n in networks:
self.addCleanup(self.client.remove_network, n['Id']) self.addCleanup(self.client.remove_network, n['Id'])
self.assertEqual(len(networks), 1) self.assertEqual(len(networks), 1)

View File

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

View File

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

View File

@ -14,7 +14,6 @@ from compose.const import LABEL_PROJECT
from compose.container import Container from compose.container import Container
from compose.project import Project from compose.project import Project
from compose.service import ConvergenceStrategy from compose.service import ConvergenceStrategy
from compose.service import Net
def build_service_dicts(service_config): def build_service_dicts(service_config):
@ -104,21 +103,6 @@ class ProjectTest(DockerClientTestCase):
db = project.get_service('db') db = project.get_service('db')
self.assertEqual(db._get_volumes_from(), [data_container.id + ':rw']) 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): def test_net_from_service(self):
project = Project.from_config( project = Project.from_config(
name='composetest', 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('data').containers(stopped=True)), 1)
self.assertEqual(len(project.get_service('console').containers()), 0) 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): def test_unscale_after_restart(self):
web = self.create_service('web') web = self.create_service('web')
project = Project('composetest', [web], self.client) project = Project('composetest', [web], self.client)
@ -510,15 +482,50 @@ class ProjectTest(DockerClientTestCase):
service = project.get_service('web') service = project.get_service('web')
self.assertEqual(len(service.containers()), 1) 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): def test_project_up_volumes(self):
vol_name = '{0:x}'.format(random.getrandbits(32)) vol_name = '{0:x}'.format(random.getrandbits(32))
full_vol_name = 'composetest_{0}'.format(vol_name) full_vol_name = 'composetest_{0}'.format(vol_name)
config_data = config.Config( config_data = config.Config(
version=2, services=[{ version=2,
services=[{
'name': 'web', 'name': 'web',
'image': 'busybox:latest', 'image': 'busybox:latest',
'command': 'top' 'command': 'top'
}], volumes={vol_name: {'driver': 'local'}} }],
volumes={vol_name: {'driver': 'local'}},
networks={},
) )
project = Project.from_config( project = Project.from_config(
@ -587,11 +594,14 @@ class ProjectTest(DockerClientTestCase):
vol_name = '{0:x}'.format(random.getrandbits(32)) vol_name = '{0:x}'.format(random.getrandbits(32))
full_vol_name = 'composetest_{0}'.format(vol_name) full_vol_name = 'composetest_{0}'.format(vol_name)
config_data = config.Config( config_data = config.Config(
version=2, services=[{ version=2,
services=[{
'name': 'web', 'name': 'web',
'image': 'busybox:latest', 'image': 'busybox:latest',
'command': 'top' 'command': 'top'
}], volumes={vol_name: {}} }],
volumes={vol_name: {}},
networks={},
) )
project = Project.from_config( project = Project.from_config(
@ -608,11 +618,14 @@ class ProjectTest(DockerClientTestCase):
vol_name = '{0:x}'.format(random.getrandbits(32)) vol_name = '{0:x}'.format(random.getrandbits(32))
full_vol_name = 'composetest_{0}'.format(vol_name) full_vol_name = 'composetest_{0}'.format(vol_name)
config_data = config.Config( config_data = config.Config(
version=2, services=[{ version=2,
services=[{
'name': 'web', 'name': 'web',
'image': 'busybox:latest', 'image': 'busybox:latest',
'command': 'top' 'command': 'top'
}], volumes={vol_name: {}} }],
volumes={vol_name: {}},
networks={},
) )
project = Project.from_config( project = Project.from_config(
@ -629,11 +642,14 @@ class ProjectTest(DockerClientTestCase):
vol_name = '{0:x}'.format(random.getrandbits(32)) vol_name = '{0:x}'.format(random.getrandbits(32))
config_data = config.Config( config_data = config.Config(
version=2, services=[{ version=2,
services=[{
'name': 'web', 'name': 'web',
'image': 'busybox:latest', 'image': 'busybox:latest',
'command': 'top' 'command': 'top'
}], volumes={vol_name: {'driver': 'foobar'}} }],
volumes={vol_name: {'driver': 'foobar'}},
networks={},
) )
project = Project.from_config( project = Project.from_config(
@ -648,11 +664,14 @@ class ProjectTest(DockerClientTestCase):
full_vol_name = 'composetest_{0}'.format(vol_name) full_vol_name = 'composetest_{0}'.format(vol_name)
config_data = config.Config( config_data = config.Config(
version=2, services=[{ version=2,
services=[{
'name': 'web', 'name': 'web',
'image': 'busybox:latest', 'image': 'busybox:latest',
'command': 'top' 'command': 'top'
}], volumes={vol_name: {'driver': 'local'}} }],
volumes={vol_name: {'driver': 'local'}},
networks={},
) )
project = Project.from_config( project = Project.from_config(
name='composetest', name='composetest',
@ -683,13 +702,16 @@ class ProjectTest(DockerClientTestCase):
full_vol_name = 'composetest_{0}'.format(vol_name) full_vol_name = 'composetest_{0}'.format(vol_name)
self.client.create_volume(vol_name) self.client.create_volume(vol_name)
config_data = config.Config( config_data = config.Config(
version=2, services=[{ version=2,
services=[{
'name': 'web', 'name': 'web',
'image': 'busybox:latest', 'image': 'busybox:latest',
'command': 'top' 'command': 'top'
}], volumes={ }],
volumes={
vol_name: {'external': True, 'external_name': vol_name} vol_name: {'external': True, 'external_name': vol_name}
} },
networks=None,
) )
project = Project.from_config( project = Project.from_config(
name='composetest', name='composetest',
@ -704,13 +726,16 @@ class ProjectTest(DockerClientTestCase):
vol_name = '{0:x}'.format(random.getrandbits(32)) vol_name = '{0:x}'.format(random.getrandbits(32))
config_data = config.Config( config_data = config.Config(
version=2, services=[{ version=2,
services=[{
'name': 'web', 'name': 'web',
'image': 'busybox:latest', 'image': 'busybox:latest',
'command': 'top' 'command': 'top'
}], volumes={ }],
volumes={
vol_name: {'external': True, 'external_name': vol_name} vol_name: {'external': True, 'external_name': vol_name}
} },
networks=None,
) )
project = Project.from_config( project = Project.from_config(
name='composetest', name='composetest',

View File

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