Add support for declaring named volumes in compose files

* Bump default API version to 1.21 (required for named volume management)
* Introduce new, versioned compose file format while maintaining support
  for current (legacy) format
* Test updates to reflect changes made to the internal API

Signed-off-by: Joffrey F <joffrey@docker.com>
This commit is contained in:
Joffrey F 2015-11-16 19:21:56 -08:00
parent 4bf2f8c4f9
commit b4be7b870f
14 changed files with 262 additions and 81 deletions

View File

@ -80,12 +80,13 @@ def get_project(base_dir, config_path=None, project_name=None, verbose=False,
config_details = config.find(base_dir, config_path) config_details = config.find(base_dir, config_path)
api_version = '1.21' if use_networking else None api_version = '1.21' if use_networking else None
return Project.from_dicts( return Project.from_config(
get_project_name(config_details.working_dir, project_name), get_project_name(config_details.working_dir, project_name),
config.load(config_details), config.load(config_details),
get_client(verbose=verbose, version=api_version), get_client(verbose=verbose, version=api_version),
use_networking=use_networking, use_networking=use_networking,
network_driver=network_driver) network_driver=network_driver
)
def get_project_name(working_dir, project_name=None): def get_project_name(working_dir, project_name=None):

View File

@ -8,8 +8,7 @@ from ..const import HTTP_TIMEOUT
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
DEFAULT_API_VERSION = '1.21'
DEFAULT_API_VERSION = '1.20'
def docker_client(version=None): def docker_client(version=None):

View File

@ -117,6 +117,17 @@ class ConfigFile(namedtuple('_ConfigFile', 'filename config')):
return cls(filename, load_yaml(filename)) return cls(filename, load_yaml(filename))
class Config(namedtuple('_Config', 'version services volumes')):
"""
: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`
"""
class ServiceConfig(namedtuple('_ServiceConfig', 'working_dir filename name config')): class ServiceConfig(namedtuple('_ServiceConfig', 'working_dir filename name config')):
@classmethod @classmethod
@ -148,6 +159,24 @@ def find(base_dir, filenames):
[ConfigFile.from_filename(f) for f in filenames]) [ConfigFile.from_filename(f) for f in filenames])
def get_config_version(config_details):
def get_version(config):
validate_top_level_object(config)
return config.config.get('version')
main_file = config_details.config_files[0]
version = get_version(main_file)
for next_file in config_details.config_files[1:]:
next_file_version = get_version(next_file)
if version != next_file_version:
raise ConfigurationError(
"Version mismatch: main file {0} specifies version {1} but "
"extension file {2} uses version {3}".format(
main_file.filename, version, next_file.filename, next_file_version
)
)
return version
def get_default_config_files(base_dir): def get_default_config_files(base_dir):
(candidates, path) = find_candidates_in_parent_dirs(SUPPORTED_FILENAMES, base_dir) (candidates, path) = find_candidates_in_parent_dirs(SUPPORTED_FILENAMES, base_dir)
@ -194,10 +223,46 @@ def load(config_details):
Return a fully interpolated, extended and validated configuration. Return a fully interpolated, extended and validated configuration.
""" """
version = get_config_version(config_details)
processed_files = []
for config_file in config_details.config_files:
processed_files.append(
process_config_file(config_file, version=version)
)
config_details = config_details._replace(config_files=processed_files)
if not version or isinstance(version, dict):
service_dicts = load_services(
config_details.working_dir, config_details.config_files
)
volumes = {}
elif version == 2:
config_files = [
ConfigFile(f.filename, f.config.get('services', {}))
for f in config_details.config_files
]
service_dicts = load_services(
config_details.working_dir, config_files
)
volumes = load_volumes(config_details.config_files)
else:
raise ConfigurationError('Invalid config version provided: {0}'.format(version))
return Config(version, service_dicts, volumes)
def load_volumes(config_files):
volumes = {}
for config_file in config_files:
for name, volume_config in config_file.config.get('volumes', {}).items():
volumes.update({name: volume_config})
return volumes
def load_services(working_dir, config_files):
def build_service(filename, service_name, service_dict): def build_service(filename, service_name, service_dict):
service_config = ServiceConfig.with_abs_paths( service_config = ServiceConfig.with_abs_paths(
config_details.working_dir, working_dir,
filename, filename,
service_name, service_name,
service_dict) service_dict)
@ -227,20 +292,20 @@ def load(config_details):
for name in all_service_names for name in all_service_names
} }
config_file = process_config_file(config_details.config_files[0]) config_file = config_files[0]
for next_file in config_details.config_files[1:]: for next_file in config_files[1:]:
next_file = process_config_file(next_file)
config = merge_services(config_file.config, next_file.config) config = merge_services(config_file.config, next_file.config)
config_file = config_file._replace(config=config) config_file = config_file._replace(config=config)
return build_services(config_file) return build_services(config_file)
def process_config_file(config_file, service_name=None): def process_config_file(config_file, service_name=None, version=None):
validate_top_level_object(config_file) validate_top_level_object(config_file)
processed_config = interpolate_environment_variables(config_file.config) processed_config = interpolate_environment_variables(config_file.config)
validate_against_fields_schema(processed_config, config_file.filename) validate_against_fields_schema(
processed_config, config_file.filename, version
)
if service_name and service_name not in processed_config: if service_name and service_name not in processed_config:
raise ConfigurationError( raise ConfigurationError(

View File

@ -0,0 +1,43 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"version": {
"enum": [2]
},
"services": {
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "fields_schema.json#/definitions/service"
}
}
},
"volumes": {
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/volume"
}
}
}
},
"definitions": {
"volume": {
"type": "object",
"properties": {
"driver": {"type": "string"},
"driver_opts": {
"type": "object",
"patternProperties": {
"^.+$": {"type": ["boolean", "string", "number"]}
},
"additionalProperties": false
}
}
}
},
"additionalProperties": false
}

View File

@ -281,11 +281,14 @@ def process_errors(errors, service_name=None):
return '\n'.join(format_error_message(error, service_name) for error in errors) return '\n'.join(format_error_message(error, service_name) for error in errors)
def validate_against_fields_schema(config, filename): def validate_against_fields_schema(config, filename, version=None):
schema_filename = "fields_schema.json"
if version:
schema_filename = "fields_schema_v{0}.json".format(version)
_validate_against_schema( _validate_against_schema(
config, config,
"fields_schema.json", schema_filename,
format_checker=["ports", "expose", "bool-value-in-mapping"], format_checker=["ports", "environment", "bool-value-in-mapping"],
filename=filename) filename=filename)

View File

@ -20,6 +20,7 @@ from .service import ConvergenceStrategy
from .service import Net from .service import Net
from .service import Service from .service import Service
from .service import ServiceNet from .service import ServiceNet
from .volume import Volume
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -29,12 +30,13 @@ class Project(object):
""" """
A collection of services. A collection of services.
""" """
def __init__(self, name, services, client, use_networking=False, network_driver=None): def __init__(self, name, services, client, 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.volumes = volumes or []
def labels(self, one_off=False): def labels(self, one_off=False):
return [ return [
@ -43,16 +45,16 @@ class Project(object):
] ]
@classmethod @classmethod
def from_dicts(cls, name, service_dicts, client, use_networking=False, network_driver=None): def from_config(cls, name, config_data, client, use_networking=False, network_driver=None):
""" """
Construct a ServiceCollection from a list of dicts representing services. Construct a Project from a config.Config object.
""" """
project = cls(name, [], client, use_networking=use_networking, network_driver=network_driver) project = cls(name, [], client, use_networking=use_networking, network_driver=network_driver)
if use_networking: if use_networking:
remove_links(service_dicts) remove_links(config_data.services)
for service_dict in service_dicts: for service_dict in config_data.services:
links = project.get_links(service_dict) links = project.get_links(service_dict)
volumes_from = project.get_volumes_from(service_dict) volumes_from = project.get_volumes_from(service_dict)
net = project.get_net(service_dict) net = project.get_net(service_dict)
@ -66,6 +68,14 @@ class Project(object):
net=net, net=net,
volumes_from=volumes_from, volumes_from=volumes_from,
**service_dict)) **service_dict))
if config_data.volumes:
for vol_name, data in config_data.volumes.items():
project.volumes.append(
Volume(
client=client, project=name, name=vol_name,
driver=data.get('driver'), driver_opts=data.get('driver_opts')
)
)
return project return project
@property @property
@ -218,6 +228,15 @@ class Project(object):
def remove_stopped(self, service_names=None, **options): def remove_stopped(self, service_names=None, **options):
parallel.parallel_remove(self.containers(service_names, stopped=True), options) parallel.parallel_remove(self.containers(service_names, stopped=True), options)
def initialize_volumes(self):
try:
for volume in self.volumes:
volume.create()
except NotFound:
raise ConfigurationError(
'Volume %s sepcifies nonexistent driver %s' % (volume.name, volume.driver)
)
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)
@ -253,6 +272,8 @@ class Project(object):
if self.use_networking and self.uses_default_network(): if self.use_networking and self.uses_default_network():
self.ensure_network_exists() self.ensure_network_exists()
self.initialize_volumes()
return [ return [
container container
for service in services for service in services

19
compose/volume.py Normal file
View File

@ -0,0 +1,19 @@
from __future__ import unicode_literals
class Volume(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 create(self):
return self.client.create_volume(self.name, self.driver, self.driver_opts)
def remove(self):
return self.client.remove_volume(self.name)
def inspect(self):
return self.client.inspect_volume(self.name)

View File

@ -1,5 +1,6 @@
-e git://github.com/docker/docker-py.git@881e24c231ab9921eb0cbd475e85706137983f89#egg=docker-py
PyYAML==3.11 PyYAML==3.11
docker-py==1.5.0 # docker-py==1.5.1
dockerpty==0.3.4 dockerpty==0.3.4
docopt==0.6.1 docopt==0.6.1
enum34==1.0.4 enum34==1.0.4

View File

@ -69,9 +69,9 @@ class ProjectTest(DockerClientTestCase):
'volumes_from': ['data'], 'volumes_from': ['data'],
}, },
}) })
project = Project.from_dicts( project = Project.from_config(
name='composetest', name='composetest',
service_dicts=service_dicts, config_data=service_dicts,
client=self.client, client=self.client,
) )
db = project.get_service('db') db = project.get_service('db')
@ -86,9 +86,9 @@ class ProjectTest(DockerClientTestCase):
name='composetest_data_container', name='composetest_data_container',
labels={LABEL_PROJECT: 'composetest'}, labels={LABEL_PROJECT: 'composetest'},
) )
project = Project.from_dicts( project = Project.from_config(
name='composetest', name='composetest',
service_dicts=build_service_dicts({ config_data=build_service_dicts({
'db': { 'db': {
'image': 'busybox:latest', 'image': 'busybox:latest',
'volumes_from': ['composetest_data_container'], 'volumes_from': ['composetest_data_container'],
@ -117,9 +117,9 @@ class ProjectTest(DockerClientTestCase):
assert project.get_network()['Name'] == network_name assert project.get_network()['Name'] == network_name
def test_net_from_service(self): def test_net_from_service(self):
project = Project.from_dicts( project = Project.from_config(
name='composetest', name='composetest',
service_dicts=build_service_dicts({ config_data=build_service_dicts({
'net': { 'net': {
'image': 'busybox:latest', 'image': 'busybox:latest',
'command': ["top"] 'command': ["top"]
@ -149,9 +149,9 @@ class ProjectTest(DockerClientTestCase):
) )
net_container.start() net_container.start()
project = Project.from_dicts( project = Project.from_config(
name='composetest', name='composetest',
service_dicts=build_service_dicts({ config_data=build_service_dicts({
'web': { 'web': {
'image': 'busybox:latest', 'image': 'busybox:latest',
'net': 'container:composetest_net_container' 'net': 'container:composetest_net_container'
@ -331,7 +331,6 @@ class ProjectTest(DockerClientTestCase):
project.up(['db']) project.up(['db'])
self.assertEqual(len(project.containers()), 1) self.assertEqual(len(project.containers()), 1)
old_db_id = project.containers()[0].id old_db_id = project.containers()[0].id
container, = project.containers() container, = project.containers()
db_volume_path = container.get_mount('/var/db')['Source'] db_volume_path = container.get_mount('/var/db')['Source']
@ -401,9 +400,9 @@ class ProjectTest(DockerClientTestCase):
self.assertEqual(len(console.containers()), 0) self.assertEqual(len(console.containers()), 0)
def test_project_up_starts_depends(self): def test_project_up_starts_depends(self):
project = Project.from_dicts( project = Project.from_config(
name='composetest', name='composetest',
service_dicts=build_service_dicts({ config_data=build_service_dicts({
'console': { 'console': {
'image': 'busybox:latest', 'image': 'busybox:latest',
'command': ["top"], 'command': ["top"],
@ -436,9 +435,9 @@ class ProjectTest(DockerClientTestCase):
self.assertEqual(len(project.get_service('console').containers()), 0) self.assertEqual(len(project.get_service('console').containers()), 0)
def test_project_up_with_no_deps(self): def test_project_up_with_no_deps(self):
project = Project.from_dicts( project = Project.from_config(
name='composetest', name='composetest',
service_dicts=build_service_dicts({ config_data=build_service_dicts({
'console': { 'console': {
'image': 'busybox:latest', 'image': 'busybox:latest',
'command': ["top"], 'command': ["top"],

View File

@ -163,6 +163,7 @@ class ServiceTest(DockerClientTestCase):
# Match the last component ("host-path"), because boot2docker symlinks /tmp # Match the last component ("host-path"), because boot2docker symlinks /tmp
actual_host_path = container.get_mount(container_path)['Source'] actual_host_path = container.get_mount(container_path)['Source']
self.assertTrue(path.basename(actual_host_path) == path.basename(host_path), self.assertTrue(path.basename(actual_host_path) == path.basename(host_path),
msg=("Last component differs: %s, %s" % (actual_host_path, host_path))) msg=("Last component differs: %s, %s" % (actual_host_path, host_path)))

View File

@ -26,10 +26,10 @@ class ProjectTestCase(DockerClientTestCase):
details = config.ConfigDetails( details = config.ConfigDetails(
'working_dir', 'working_dir',
[config.ConfigFile(None, cfg)]) [config.ConfigFile(None, cfg)])
return Project.from_dicts( return Project.from_config(
name='composetest', name='composetest',
client=self.client, client=self.client,
service_dicts=config.load(details)) config_data=config.load(details))
class BasicProjectTest(ProjectTestCase): class BasicProjectTest(ProjectTestCase):

View File

@ -39,6 +39,10 @@ class DockerClientTestCase(unittest.TestCase):
for i in self.client.images( for i in self.client.images(
filters={'label': 'com.docker.compose.test_image'}): filters={'label': 'com.docker.compose.test_image'}):
self.client.remove_image(i) self.client.remove_image(i)
volumes = self.client.volumes().get('Volumes') or []
for v in volumes:
if 'composetests_' in v['Name']:
self.client.remove_volume(v['Name'])
def create_service(self, name, **kwargs): def create_service(self, name, **kwargs):
if 'image' not in kwargs and 'build' not in kwargs: if 'image' not in kwargs and 'build' not in kwargs:

View File

@ -51,7 +51,7 @@ class ConfigTest(unittest.TestCase):
'tests/fixtures/extends', 'tests/fixtures/extends',
'common.yml' 'common.yml'
) )
) ).services
self.assertEqual( self.assertEqual(
service_sort(service_dicts), service_sort(service_dicts),
@ -143,7 +143,7 @@ class ConfigTest(unittest.TestCase):
}) })
details = config.ConfigDetails('.', [base_file, override_file]) details = config.ConfigDetails('.', [base_file, override_file])
service_dicts = config.load(details) service_dicts = config.load(details).services
expected = [ expected = [
{ {
'name': 'web', 'name': 'web',
@ -207,7 +207,7 @@ class ConfigTest(unittest.TestCase):
labels: ['label=one'] labels: ['label=one']
""") """)
with tmpdir.as_cwd(): with tmpdir.as_cwd():
service_dicts = config.load(details) service_dicts = config.load(details).services
expected = [ expected = [
{ {
@ -260,7 +260,7 @@ class ConfigTest(unittest.TestCase):
build_config_details( build_config_details(
{valid_name: {'image': 'busybox'}}, {valid_name: {'image': 'busybox'}},
'tests/fixtures/extends', 'tests/fixtures/extends',
'common.yml')) 'common.yml')).services
assert services[0]['name'] == valid_name assert services[0]['name'] == valid_name
def test_config_hint(self): def test_config_hint(self):
@ -451,7 +451,7 @@ class ConfigTest(unittest.TestCase):
'working_dir', 'working_dir',
'filename.yml' 'filename.yml'
) )
) ).services
self.assertEqual(service[0]['expose'], expose) self.assertEqual(service[0]['expose'], expose)
def test_valid_config_oneof_string_or_list(self): def test_valid_config_oneof_string_or_list(self):
@ -466,7 +466,7 @@ class ConfigTest(unittest.TestCase):
'working_dir', 'working_dir',
'filename.yml' 'filename.yml'
) )
) ).services
self.assertEqual(service[0]['entrypoint'], entrypoint) self.assertEqual(service[0]['entrypoint'], entrypoint)
@mock.patch('compose.config.validation.log') @mock.patch('compose.config.validation.log')
@ -496,7 +496,7 @@ class ConfigTest(unittest.TestCase):
'working_dir', 'working_dir',
'filename.yml' 'filename.yml'
) )
) ).services
self.assertEqual(services[0]['environment']['SPRING_JPA_HIBERNATE_DDL-AUTO'], 'none') self.assertEqual(services[0]['environment']['SPRING_JPA_HIBERNATE_DDL-AUTO'], 'none')
def test_load_yaml_with_yaml_error(self): def test_load_yaml_with_yaml_error(self):
@ -655,7 +655,7 @@ class InterpolationTest(unittest.TestCase):
service_dicts = config.load( service_dicts = config.load(
config.find('tests/fixtures/environment-interpolation', None), config.find('tests/fixtures/environment-interpolation', None),
) ).services
self.assertEqual(service_dicts, [ self.assertEqual(service_dicts, [
{ {
@ -722,7 +722,7 @@ class InterpolationTest(unittest.TestCase):
'.', '.',
None, None,
) )
)[0] ).services[0]
self.assertEquals(service_dict['environment']['POSTGRES_PASSWORD'], '') self.assertEquals(service_dict['environment']['POSTGRES_PASSWORD'], '')
@ -734,11 +734,15 @@ class VolumeConfigTest(unittest.TestCase):
@mock.patch.dict(os.environ) @mock.patch.dict(os.environ)
def test_volume_binding_with_environment_variable(self): def test_volume_binding_with_environment_variable(self):
os.environ['VOLUME_PATH'] = '/host/path' os.environ['VOLUME_PATH'] = '/host/path'
d = config.load(build_config_details(
{'foo': {'build': '.', 'volumes': ['${VOLUME_PATH}:/container/path']}}, d = config.load(
'.', build_config_details(
))[0] {'foo': {'build': '.', 'volumes': ['${VOLUME_PATH}:/container/path']}},
self.assertEqual(d['volumes'], [VolumeSpec.parse('/host/path:/container/path')]) '.',
None,
)
).services[0]
self.assertEqual(d['volumes'], ['/host/path:/container/path'])
@pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='posix paths') @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='posix paths')
@mock.patch.dict(os.environ) @mock.patch.dict(os.environ)
@ -1012,7 +1016,7 @@ class MemoryOptionsTest(unittest.TestCase):
'tests/fixtures/extends', 'tests/fixtures/extends',
'common.yml' 'common.yml'
) )
) ).services
self.assertEqual(service_dict[0]['memswap_limit'], 2000000) self.assertEqual(service_dict[0]['memswap_limit'], 2000000)
def test_memswap_can_be_a_string(self): def test_memswap_can_be_a_string(self):
@ -1022,7 +1026,7 @@ class MemoryOptionsTest(unittest.TestCase):
'tests/fixtures/extends', 'tests/fixtures/extends',
'common.yml' 'common.yml'
) )
) ).services
self.assertEqual(service_dict[0]['memswap_limit'], "512M") self.assertEqual(service_dict[0]['memswap_limit'], "512M")
@ -1126,24 +1130,21 @@ class EnvTest(unittest.TestCase):
{'foo': {'build': '.', 'volumes': ['$HOSTENV:$CONTAINERENV']}}, {'foo': {'build': '.', 'volumes': ['$HOSTENV:$CONTAINERENV']}},
"tests/fixtures/env", "tests/fixtures/env",
) )
)[0]
self.assertEqual( ).services[0]
set(service_dict['volumes']), self.assertEqual(set(service_dict['volumes']), set(['/tmp:/host/tmp']))
set([VolumeSpec.parse('/tmp:/host/tmp')]))
service_dict = config.load( service_dict = config.load(
build_config_details( build_config_details(
{'foo': {'build': '.', 'volumes': ['/opt${HOSTENV}:/opt${CONTAINERENV}']}}, {'foo': {'build': '.', 'volumes': ['/opt${HOSTENV}:/opt${CONTAINERENV}']}},
"tests/fixtures/env", "tests/fixtures/env",
) )
)[0] ).services[0]
self.assertEqual( self.assertEqual(set(service_dict['volumes']), set(['/opt/tmp:/opt/host/tmp']))
set(service_dict['volumes']),
set([VolumeSpec.parse('/opt/tmp:/opt/host/tmp')]))
def load_from_filename(filename): def load_from_filename(filename):
return config.load(config.find('.', [filename])) return config.load(config.find('.', [filename])).services
class ExtendsTest(unittest.TestCase): class ExtendsTest(unittest.TestCase):
@ -1313,7 +1314,7 @@ class ExtendsTest(unittest.TestCase):
'tests/fixtures/extends', 'tests/fixtures/extends',
'common.yml' 'common.yml'
) )
) ).services
self.assertEquals(len(service), 1) self.assertEquals(len(service), 1)
self.assertIsInstance(service[0], dict) self.assertIsInstance(service[0], dict)

View File

@ -4,6 +4,7 @@ import docker
from .. import mock from .. import mock
from .. import unittest from .. import unittest
from compose.config.config import Config
from compose.config.types import VolumeFromSpec from compose.config.types import VolumeFromSpec
from compose.const import LABEL_SERVICE from compose.const import LABEL_SERVICE
from compose.container import Container from compose.container import Container
@ -18,7 +19,7 @@ class ProjectTest(unittest.TestCase):
self.mock_client = mock.create_autospec(docker.Client) self.mock_client = mock.create_autospec(docker.Client)
def test_from_dict(self): def test_from_dict(self):
project = Project.from_dicts('composetest', [ project = Project.from_config('composetest', Config(None, [
{ {
'name': 'web', 'name': 'web',
'image': 'busybox:latest' 'image': 'busybox:latest'
@ -27,15 +28,38 @@ class ProjectTest(unittest.TestCase):
'name': 'db', 'name': 'db',
'image': 'busybox:latest' 'image': 'busybox:latest'
}, },
], None) ], None), 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')
self.assertEqual(project.get_service('db').name, 'db') self.assertEqual(project.get_service('db').name, 'db')
self.assertEqual(project.get_service('db').options['image'], 'busybox:latest') self.assertEqual(project.get_service('db').options['image'], 'busybox:latest')
def test_from_dict_sorts_in_dependency_order(self):
project = Project.from_config('composetest', Config(None, [
{
'name': 'web',
'image': 'busybox:latest',
'links': ['db'],
},
{
'name': 'db',
'image': 'busybox:latest',
'volumes_from': ['volume']
},
{
'name': 'volume',
'image': 'busybox:latest',
'volumes': ['/tmp'],
}
], None), None)
self.assertEqual(project.services[0].name, 'volume')
self.assertEqual(project.services[1].name, 'db')
self.assertEqual(project.services[2].name, 'web')
def test_from_config(self): def test_from_config(self):
dicts = [ dicts = Config(None, [
{ {
'name': 'web', 'name': 'web',
'image': 'busybox:latest', 'image': 'busybox:latest',
@ -44,8 +68,8 @@ class ProjectTest(unittest.TestCase):
'name': 'db', 'name': 'db',
'image': 'busybox:latest', 'image': 'busybox:latest',
}, },
] ], None)
project = Project.from_dicts('composetest', dicts, None) project = Project.from_config('composetest', dicts, 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')
@ -141,13 +165,13 @@ 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_dicts('test', [ project = Project.from_config('test', Config(None, [
{ {
'name': 'test', 'name': 'test',
'image': 'busybox:latest', 'image': 'busybox:latest',
'volumes_from': [VolumeFromSpec('aaa', 'rw')] 'volumes_from': [VolumeFromSpec('aaa', 'rw')]
} }
], self.mock_client) ], None), self.mock_client)
self.assertEqual(project.get_service('test')._get_volumes_from(), [container_id + ":rw"]) self.assertEqual(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):
@ -160,7 +184,7 @@ class ProjectTest(unittest.TestCase):
"Image": 'busybox:latest' "Image": 'busybox:latest'
} }
] ]
project = Project.from_dicts('test', [ project = Project.from_config('test', Config(None, [
{ {
'name': 'vol', 'name': 'vol',
'image': 'busybox:latest' 'image': 'busybox:latest'
@ -170,13 +194,13 @@ class ProjectTest(unittest.TestCase):
'image': 'busybox:latest', 'image': 'busybox:latest',
'volumes_from': [VolumeFromSpec('vol', 'rw')] 'volumes_from': [VolumeFromSpec('vol', 'rw')]
} }
], self.mock_client) ], None), self.mock_client)
self.assertEqual(project.get_service('test')._get_volumes_from(), [container_name + ":rw"]) self.assertEqual(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_dicts('test', [ project = Project.from_config('test', Config(None, [
{ {
'name': 'vol', 'name': 'vol',
'image': 'busybox:latest' 'image': 'busybox:latest'
@ -186,7 +210,7 @@ class ProjectTest(unittest.TestCase):
'image': 'busybox:latest', 'image': 'busybox:latest',
'volumes_from': [VolumeFromSpec('vol', 'rw')] 'volumes_from': [VolumeFromSpec('vol', 'rw')]
} }
], None) ], None), 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)
@ -196,12 +220,12 @@ class ProjectTest(unittest.TestCase):
[container_ids[0] + ':rw']) [container_ids[0] + ':rw'])
def test_net_unset(self): def test_net_unset(self):
project = Project.from_dicts('test', [ project = Project.from_config('test', Config(None, [
{ {
'name': 'test', 'name': 'test',
'image': 'busybox:latest', 'image': 'busybox:latest',
} }
], self.mock_client) ], None), self.mock_client)
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({}))
@ -210,13 +234,13 @@ 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_dicts('test', [ project = Project.from_config('test', Config(None, [
{ {
'name': 'test', 'name': 'test',
'image': 'busybox:latest', 'image': 'busybox:latest',
'net': 'container:aaa' 'net': 'container:aaa'
} }
], self.mock_client) ], None), self.mock_client)
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)
@ -230,7 +254,7 @@ class ProjectTest(unittest.TestCase):
"Image": 'busybox:latest' "Image": 'busybox:latest'
} }
] ]
project = Project.from_dicts('test', [ project = Project.from_config('test', Config(None, [
{ {
'name': 'aaa', 'name': 'aaa',
'image': 'busybox:latest' 'image': 'busybox:latest'
@ -240,7 +264,7 @@ class ProjectTest(unittest.TestCase):
'image': 'busybox:latest', 'image': 'busybox:latest',
'net': 'container:aaa' 'net': 'container:aaa'
} }
], self.mock_client) ], None), self.mock_client)
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)
@ -285,12 +309,12 @@ class ProjectTest(unittest.TestCase):
}, },
}, },
} }
project = Project.from_dicts( project = Project.from_config(
'test', 'test',
[{ Config(None, [{
'name': 'web', 'name': 'web',
'image': 'busybox:latest', 'image': 'busybox:latest',
}], }], None),
self.mock_client, self.mock_client,
) )
self.assertEqual([c.id for c in project.containers()], ['1']) self.assertEqual([c.id for c in project.containers()], ['1'])