diff --git a/compose/config/fields_schema_v2.json b/compose/config/fields_schema_v2.json index 22ff839fb..61bd7628b 100644 --- a/compose/config/fields_schema_v2.json +++ b/compose/config/fields_schema_v2.json @@ -41,6 +41,13 @@ "^.+$": {"type": ["string", "number"]} }, "additionalProperties": false + }, + "external": { + "type": ["boolean", "object"], + "properties": { + "name": {"type": "string"} + }, + "additionalProperties": false } } } diff --git a/compose/project.py b/compose/project.py index f5bb8bebf..49b54e10f 100644 --- a/compose/project.py +++ b/compose/project.py @@ -77,7 +77,9 @@ class Project(object): project.volumes.append( Volume( client=client, project=name, name=vol_name, - driver=data.get('driver'), driver_opts=data.get('driver_opts') + driver=data.get('driver'), + driver_opts=data.get('driver_opts'), + external=data.get('external', False) ) ) return project @@ -235,11 +237,17 @@ class Project(object): def initialize_volumes(self): try: for volume in self.volumes: - if volume.is_user_created: + if volume.external: log.info( - 'Found user-created volume "{0}". No new namespaced ' + 'Volume {0} declared as external. No new ' 'volume will be created.'.format(volume.name) ) + if not volume.exists(): + raise ConfigurationError( + 'Volume {0} declared as external, but could not be' + ' found. Please create the volume manually and try' + ' again.'.format(volume.full_name) + ) continue volume.create() except NotFound: diff --git a/compose/volume.py b/compose/volume.py index 9bd98fa5b..64671ca9b 100644 --- a/compose/volume.py +++ b/compose/volume.py @@ -5,12 +5,19 @@ from docker.errors import NotFound class Volume(object): - def __init__(self, client, project, name, driver=None, driver_opts=None): + def __init__(self, client, project, name, driver=None, driver_opts=None, + external=False): self.client = client self.project = project self.name = name self.driver = driver self.driver_opts = driver_opts + self.external_name = None + if external: + if isinstance(external, dict): + self.external_name = external.get('name') + else: + self.external_name = self.name def create(self): return self.client.create_volume( @@ -23,15 +30,19 @@ class Volume(object): def inspect(self): return self.client.inspect_volume(self.full_name) - @property - def is_user_created(self): + def exists(self): try: - self.client.inspect_volume(self.name) + self.inspect() except NotFound: return False - return True + @property + def external(self): + return bool(self.external_name) + @property def full_name(self): + if self.external_name: + return self.external_name return '{0}_{1}'.format(self.project, self.name) diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index d1b1fdf07..36b736b42 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -677,7 +677,7 @@ class ProjectTest(DockerClientTestCase): vol_name ) in str(e.exception) - def test_initialize_volumes_user_created_volumes(self): + def test_initialize_volumes_external_volumes(self): # Use composetest_ prefix so it gets garbage-collected in tearDown() vol_name = 'composetest_{0:x}'.format(random.getrandbits(32)) full_vol_name = 'composetest_{0}'.format(vol_name) @@ -687,7 +687,7 @@ class ProjectTest(DockerClientTestCase): 'name': 'web', 'image': 'busybox:latest', 'command': 'top' - }], volumes={vol_name: {'driver': 'local'}} + }], volumes={vol_name: {'external': True}} ) project = Project.from_config( name='composetest', @@ -697,3 +697,23 @@ class ProjectTest(DockerClientTestCase): with self.assertRaises(NotFound): self.client.inspect_volume(full_vol_name) + + def test_initialize_volumes_inexistent_external_volume(self): + vol_name = '{0:x}'.format(random.getrandbits(32)) + + config_data = config.Config( + version=2, services=[{ + 'name': 'web', + 'image': 'busybox:latest', + 'command': 'top' + }], volumes={vol_name: {'external': True}} + ) + project = Project.from_config( + name='composetest', + config_data=config_data, client=self.client + ) + with self.assertRaises(config.ConfigurationError) as e: + project.initialize_volumes() + assert 'Volume {0} declared as external'.format( + vol_name + ) in str(e.exception) diff --git a/tests/integration/volume_test.py b/tests/integration/volume_test.py index 8bcce0e14..fbb4aaa27 100644 --- a/tests/integration/volume_test.py +++ b/tests/integration/volume_test.py @@ -18,9 +18,10 @@ class VolumeTest(DockerClientTestCase): except DockerException: pass - def create_volume(self, name, driver=None, opts=None): + def create_volume(self, name, driver=None, opts=None, external=False): vol = Volume( - self.client, 'composetest', name, driver=driver, driver_opts=opts + self.client, 'composetest', name, driver=driver, driver_opts=opts, + external=external ) self.tmp_volumes.append(vol) return vol @@ -55,12 +56,19 @@ class VolumeTest(DockerClientTestCase): volumes = self.client.volumes()['Volumes'] assert len([v for v in volumes if v['Name'] == vol.full_name]) == 0 - def test_is_user_created(self): - vol = Volume(self.client, 'composetest', 'uservolume01') - try: - self.client.create_volume('uservolume01') - assert vol.is_user_created is True - finally: - self.client.remove_volume('uservolume01') - vol2 = Volume(self.client, 'composetest', 'volume01') - assert vol2.is_user_created is False + def test_external_volume(self): + vol = self.create_volume('volume01', external=True) + assert vol.external is True + assert vol.full_name == vol.name + vol.create() + info = vol.inspect() + assert info['Name'] == vol.name + + def test_external_aliased_volume(self): + alias_name = 'alias01' + vol = self.create_volume('volume01', external={'name': alias_name}) + assert vol.external is True + assert vol.full_name == alias_name + vol.create() + info = vol.inspect() + assert info['Name'] == alias_name diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 25483e526..679125bc9 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -775,6 +775,24 @@ class ConfigTest(unittest.TestCase): 'extends': {'service': 'foo'} } + def test_external_volume_config(self): + config_details = build_config_details({ + 'version': 2, + 'services': { + 'bogus': {'image': 'busybox'} + }, + 'volumes': { + 'ext': {'external': True}, + 'ext2': {'external': {'name': 'aliased'}} + } + }) + config_result = config.load(config_details) + volumes = config_result.volumes + assert 'ext' in volumes + assert volumes['ext']['external'] is True + assert 'ext2' in volumes + assert volumes['ext2']['external']['name'] == 'aliased' + class PortsTest(unittest.TestCase): INVALID_PORTS_TYPES = [