From 9cb58b796e2ea70f37ea857909fa4a3044d8861b Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 12 Jan 2016 16:53:49 -0800 Subject: [PATCH] Implement ability to specify external volumes External volumes are created and managed by the user. They are not namespaced. They are expected to exist at the beginning of the up phase. Signed-off-by: Joffrey F --- compose/config/fields_schema_v2.json | 7 +++++++ compose/project.py | 14 ++++++++++--- compose/volume.py | 21 ++++++++++++++----- tests/integration/project_test.py | 24 ++++++++++++++++++++-- tests/integration/volume_test.py | 30 ++++++++++++++++++---------- tests/unit/config/config_test.py | 18 +++++++++++++++++ 6 files changed, 93 insertions(+), 21 deletions(-) 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 = [