diff --git a/compose/config/validation.py b/compose/config/validation.py index 617c95b6a..6e9439742 100644 --- a/compose/config/validation.py +++ b/compose/config/validation.py @@ -290,7 +290,7 @@ def validate_against_fields_schema(config, filename, version): _validate_against_schema( config, schema_filename, - format_checker=["ports", "environment", "bool-value-in-mapping"], + format_checker=["ports", "expose", "bool-value-in-mapping"], filename=filename) diff --git a/compose/service.py b/compose/service.py index 251620e97..24fa63942 100644 --- a/compose/service.py +++ b/compose/service.py @@ -868,6 +868,7 @@ def get_container_data_volumes(container, volumes_option): continue mount = container_mounts.get(volume.internal) + # New volume, doesn't exist in the old container if not mount: continue diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index 4107c6cff..b4acda402 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import random + from .testcases import DockerClientTestCase from compose.cli.docker_client import docker_client from compose.config import config @@ -508,3 +510,81 @@ class ProjectTest(DockerClientTestCase): project.up() service = project.get_service('web') self.assertEqual(len(service.containers()), 1) + + def test_project_up_volumes(self): + vol_name = 'composetests_{0:x}'.format(random.getrandbits(32)) + config_data = config.Config( + 2, [{ + 'name': 'web', + 'image': 'busybox:latest', + 'command': 'top' + }], {vol_name: {'driver': 'local'}} + ) + + project = Project.from_config( + name='composetest', + config_data=config_data, client=self.client + ) + project.up() + self.assertEqual(len(project.containers()), 1) + + volume_data = self.client.inspect_volume(vol_name) + self.assertEqual(volume_data['Name'], vol_name) + self.assertEqual(volume_data['Driver'], 'local') + + def test_initialize_volumes(self): + vol_name = 'composetests_{0:x}'.format(random.getrandbits(32)) + config_data = config.Config( + 2, [{ + 'name': 'web', + 'image': 'busybox:latest', + 'command': 'top' + }], {vol_name: {}} + ) + + project = Project.from_config( + name='composetest', + config_data=config_data, client=self.client + ) + project.initialize_volumes() + + volume_data = self.client.inspect_volume(vol_name) + self.assertEqual(volume_data['Name'], vol_name) + self.assertEqual(volume_data['Driver'], 'local') + + def test_project_up_implicit_volume_driver(self): + vol_name = 'composetests_{0:x}'.format(random.getrandbits(32)) + config_data = config.Config( + 2, [{ + 'name': 'web', + 'image': 'busybox:latest', + 'command': 'top' + }], {vol_name: {}} + ) + + project = Project.from_config( + name='composetest', + config_data=config_data, client=self.client + ) + project.up() + + volume_data = self.client.inspect_volume(vol_name) + self.assertEqual(volume_data['Name'], vol_name) + self.assertEqual(volume_data['Driver'], 'local') + + def test_project_up_invalid_volume_driver(self): + vol_name = 'composetests_{0:x}'.format(random.getrandbits(32)) + config_data = config.Config( + 2, [{ + 'name': 'web', + 'image': 'busybox:latest', + 'command': 'top' + }], {vol_name: {'driver': 'foobar'}} + ) + + project = Project.from_config( + name='composetest', + config_data=config_data, client=self.client + ) + with self.assertRaises(config.ConfigurationError): + project.initialize_volumes() diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 426146ccf..dac573ed1 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -26,7 +26,7 @@ def make_service_dict(name, service_dict, working_dir, filename=None): working_dir=working_dir, filename=filename, name=name, - config=service_dict)) + config=service_dict), version=1) return config.process_service(resolver.run()) @@ -68,6 +68,85 @@ class ConfigTest(unittest.TestCase): ]) ) + def test_load_v2(self): + config_data = config.load( + build_config_details({ + 'version': 2, + 'services': { + 'foo': {'image': 'busybox'}, + 'bar': {'image': 'busybox', 'environment': ['FOO=1']}, + }, + 'volumes': { + 'hello': { + 'driver': 'default', + 'driver_opts': {'beep': 'boop'} + } + } + }, 'working_dir', 'filename.yml') + ) + service_dicts = config_data.services + volume_dict = config_data.volumes + self.assertEqual( + service_sort(service_dicts), + service_sort([ + { + 'name': 'bar', + 'image': 'busybox', + 'environment': {'FOO': '1'}, + }, + { + 'name': 'foo', + 'image': 'busybox', + } + ]) + ) + self.assertEqual(volume_dict, { + 'hello': { + 'driver': 'default', + 'driver_opts': {'beep': 'boop'} + } + }) + + def test_load_service_with_name_version(self): + config_data = config.load( + build_config_details({ + 'version': { + 'image': 'busybox' + } + }, 'working_dir', 'filename.yml') + ) + service_dicts = config_data.services + self.assertEqual( + service_sort(service_dicts), + service_sort([ + { + 'name': 'version', + 'image': 'busybox', + } + ]) + ) + + def test_load_invalid_version(self): + with self.assertRaises(ConfigurationError): + config.load( + build_config_details({ + 'version': 18, + 'services': { + 'foo': {'image': 'busybox'} + } + }, 'working_dir', 'filename.yml') + ) + + with self.assertRaises(ConfigurationError): + config.load( + build_config_details({ + 'version': 'two point oh', + 'services': { + 'foo': {'image': 'busybox'} + } + }, 'working_dir', 'filename.yml') + ) + def test_load_throws_error_when_not_dict(self): with self.assertRaises(ConfigurationError): config.load( @@ -78,6 +157,16 @@ class ConfigTest(unittest.TestCase): ) ) + def test_load_throws_error_when_not_dict_v2(self): + with self.assertRaises(ConfigurationError): + config.load( + build_config_details( + {'version': 2, 'services': {'web': 'busybox:latest'}}, + 'working_dir', + 'filename.yml' + ) + ) + def test_load_config_invalid_service_names(self): for invalid_name in ['?not?allowed', ' ', '', '!', '/', '\xe2']: with pytest.raises(ConfigurationError) as exc: @@ -87,6 +176,17 @@ class ConfigTest(unittest.TestCase): 'filename.yml')) assert 'Invalid service name \'%s\'' % invalid_name in exc.exconly() + def test_config_invalid_service_names_v2(self): + for invalid_name in ['?not?allowed', ' ', '', '!', '/', '\xe2']: + with pytest.raises(ConfigurationError) as exc: + config.load( + build_config_details({ + 'version': 2, + 'services': {invalid_name: {'image': 'busybox'}} + }, 'working_dir', 'filename.yml') + ) + assert 'Invalid service name \'%s\'' % invalid_name in exc.exconly() + def test_load_with_invalid_field_name(self): config_details = build_config_details( {'web': {'image': 'busybox', 'name': 'bogus'}}, @@ -120,6 +220,22 @@ class ConfigTest(unittest.TestCase): ) ) + def test_config_integer_service_name_raise_validation_error_v2(self): + expected_error_msg = ("In file 'filename.yml' service name: 1 needs to " + "be a string, eg '1'") + + with self.assertRaisesRegexp(ConfigurationError, expected_error_msg): + config.load( + build_config_details( + { + 'version': 2, + 'services': {1: {'image': 'busybox'}} + }, + 'working_dir', + 'filename.yml' + ) + ) + @pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash') def test_load_with_multiple_files(self): base_file = config.ConfigFile( @@ -248,12 +364,55 @@ class ConfigTest(unittest.TestCase): 'volumes': ['/tmp'], } }) - services = config.load(config_details) + services = config.load(config_details).services assert services[0]['name'] == 'volume' assert services[1]['name'] == 'db' assert services[2]['name'] == 'web' + def test_load_with_multiple_files_v2(self): + base_file = config.ConfigFile( + 'base.yaml', + { + 'version': 2, + 'services': { + 'web': { + 'image': 'example/web', + 'links': ['db'], + }, + 'db': { + 'image': 'example/db', + } + }, + }) + override_file = config.ConfigFile( + 'override.yaml', + { + 'version': 2, + 'services': { + 'web': { + 'build': '/', + 'volumes': ['/home/user/project:/code'], + }, + } + }) + details = config.ConfigDetails('.', [base_file, override_file]) + + service_dicts = config.load(details).services + expected = [ + { + 'name': 'web', + 'build': os.path.abspath('/'), + 'links': ['db'], + 'volumes': [VolumeSpec.parse('/home/user/project:/code')], + }, + { + 'name': 'db', + 'image': 'example/db', + }, + ] + self.assertEqual(service_sort(service_dicts), service_sort(expected)) + def test_config_valid_service_names(self): for valid_name in ['_', '-', '.__.', '_what-up.', 'what_.up----', 'whatup']: services = config.load( @@ -742,7 +901,7 @@ class VolumeConfigTest(unittest.TestCase): None, ) ).services[0] - self.assertEqual(d['volumes'], ['/host/path:/container/path']) + self.assertEqual(d['volumes'], [VolumeSpec.parse('/host/path:/container/path')]) @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='posix paths') @mock.patch.dict(os.environ) @@ -1130,9 +1289,10 @@ class EnvTest(unittest.TestCase): {'foo': {'build': '.', 'volumes': ['$HOSTENV:$CONTAINERENV']}}, "tests/fixtures/env", ) - ).services[0] - self.assertEqual(set(service_dict['volumes']), set(['/tmp:/host/tmp'])) + self.assertEqual( + set(service_dict['volumes']), + set([VolumeSpec.parse('/tmp:/host/tmp')])) service_dict = config.load( build_config_details( @@ -1140,7 +1300,9 @@ class EnvTest(unittest.TestCase): "tests/fixtures/env", ) ).services[0] - self.assertEqual(set(service_dict['volumes']), set(['/opt/tmp:/opt/host/tmp'])) + self.assertEqual( + set(service_dict['volumes']), + set([VolumeSpec.parse('/opt/tmp:/opt/host/tmp')])) def load_from_filename(filename): @@ -1595,7 +1757,7 @@ class BuildPathTest(unittest.TestCase): for valid_url in valid_urls: service_dict = config.load(build_config_details({ 'validurl': {'build': valid_url}, - }, '.', None)) + }, '.', None)).services assert service_dict[0]['build'] == valid_url def test_invalid_url_in_build_path(self): diff --git a/tests/unit/project_test.py b/tests/unit/project_test.py index c0ed5e33a..4bf5f4636 100644 --- a/tests/unit/project_test.py +++ b/tests/unit/project_test.py @@ -35,29 +35,6 @@ class ProjectTest(unittest.TestCase): self.assertEqual(project.get_service('db').name, 'db') 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): dicts = Config(None, [ {