浏览代码

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 <[email protected]>
Joffrey F 9 年之前
父节点
当前提交
9cb58b796e

+ 7 - 0
compose/config/fields_schema_v2.json

@@ -41,6 +41,13 @@
             "^.+$": {"type": ["string", "number"]}
             "^.+$": {"type": ["string", "number"]}
           },
           },
           "additionalProperties": false
           "additionalProperties": false
+        },
+        "external": {
+          "type": ["boolean", "object"],
+          "properties": {
+            "name": {"type": "string"}
+          },
+          "additionalProperties": false
         }
         }
       }
       }
     }
     }

+ 11 - 3
compose/project.py

@@ -77,7 +77,9 @@ class Project(object):
                 project.volumes.append(
                 project.volumes.append(
                     Volume(
                     Volume(
                         client=client, project=name, name=vol_name,
                         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
         return project
@@ -235,11 +237,17 @@ class Project(object):
     def initialize_volumes(self):
     def initialize_volumes(self):
         try:
         try:
             for volume in self.volumes:
             for volume in self.volumes:
-                if volume.is_user_created:
+                if volume.external:
                     log.info(
                     log.info(
-                        'Found user-created volume "{0}". No new namespaced '
+                        'Volume {0} declared as external. No new '
                         'volume will be created.'.format(volume.name)
                         '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
                     continue
                 volume.create()
                 volume.create()
         except NotFound:
         except NotFound:

+ 16 - 5
compose/volume.py

@@ -5,12 +5,19 @@ from docker.errors import NotFound
 
 
 
 
 class Volume(object):
 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.client = client
         self.project = project
         self.project = project
         self.name = name
         self.name = name
         self.driver = driver
         self.driver = driver
         self.driver_opts = driver_opts
         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):
     def create(self):
         return self.client.create_volume(
         return self.client.create_volume(
@@ -23,15 +30,19 @@ class Volume(object):
     def inspect(self):
     def inspect(self):
         return self.client.inspect_volume(self.full_name)
         return self.client.inspect_volume(self.full_name)
 
 
-    @property
-    def is_user_created(self):
+    def exists(self):
         try:
         try:
-            self.client.inspect_volume(self.name)
+            self.inspect()
         except NotFound:
         except NotFound:
             return False
             return False
-
         return True
         return True
 
 
+    @property
+    def external(self):
+        return bool(self.external_name)
+
     @property
     @property
     def full_name(self):
     def full_name(self):
+        if self.external_name:
+            return self.external_name
         return '{0}_{1}'.format(self.project, self.name)
         return '{0}_{1}'.format(self.project, self.name)

+ 22 - 2
tests/integration/project_test.py

@@ -677,7 +677,7 @@ class ProjectTest(DockerClientTestCase):
             vol_name
             vol_name
         ) in str(e.exception)
         ) 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()
         # Use composetest_ prefix so it gets garbage-collected in tearDown()
         vol_name = 'composetest_{0:x}'.format(random.getrandbits(32))
         vol_name = 'composetest_{0:x}'.format(random.getrandbits(32))
         full_vol_name = 'composetest_{0}'.format(vol_name)
         full_vol_name = 'composetest_{0}'.format(vol_name)
@@ -687,7 +687,7 @@ class ProjectTest(DockerClientTestCase):
                 'name': 'web',
                 'name': 'web',
                 'image': 'busybox:latest',
                 'image': 'busybox:latest',
                 'command': 'top'
                 'command': 'top'
-            }], volumes={vol_name: {'driver': 'local'}}
+            }], volumes={vol_name: {'external': True}}
         )
         )
         project = Project.from_config(
         project = Project.from_config(
             name='composetest',
             name='composetest',
@@ -697,3 +697,23 @@ class ProjectTest(DockerClientTestCase):
 
 
         with self.assertRaises(NotFound):
         with self.assertRaises(NotFound):
             self.client.inspect_volume(full_vol_name)
             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)

+ 19 - 11
tests/integration/volume_test.py

@@ -18,9 +18,10 @@ class VolumeTest(DockerClientTestCase):
             except DockerException:
             except DockerException:
                 pass
                 pass
 
 
-    def create_volume(self, name, driver=None, opts=None):
+    def create_volume(self, name, driver=None, opts=None, external=False):
         vol = Volume(
         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)
         self.tmp_volumes.append(vol)
         return vol
         return vol
@@ -55,12 +56,19 @@ class VolumeTest(DockerClientTestCase):
         volumes = self.client.volumes()['Volumes']
         volumes = self.client.volumes()['Volumes']
         assert len([v for v in volumes if v['Name'] == vol.full_name]) == 0
         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

+ 18 - 0
tests/unit/config/config_test.py

@@ -775,6 +775,24 @@ class ConfigTest(unittest.TestCase):
             'extends': {'service': 'foo'}
             '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):
 class PortsTest(unittest.TestCase):
     INVALID_PORTS_TYPES = [
     INVALID_PORTS_TYPES = [