Browse Source

Merge pull request #2647 from shin-/preexisting_volume_detection_rb1

Support for external volumes
Joffrey F 9 năm trước cách đây
mục cha
commit
153185eadb

+ 7 - 0
compose/config/config.py

@@ -273,6 +273,13 @@ def load_volumes(config_files):
     for config_file in config_files:
         for name, volume_config in config_file.config.get('volumes', {}).items():
             volumes.update({name: volume_config})
+            external = volume_config.get('external')
+            if external:
+                if isinstance(external, dict):
+                    volume_config['external_name'] = external.get('name')
+                else:
+                    volume_config['external_name'] = name
+
     return volumes
 
 

+ 26 - 11
compose/config/fields_schema_v2.json

@@ -32,17 +32,32 @@
   "definitions": {
     "volume": {
       "id": "#/definitions/volume",
-      "type": "object",
-      "properties": {
-        "driver": {"type": "string"},
-        "driver_opts": {
-          "type": "object",
-          "patternProperties": {
-            "^.+$": {"type": ["string", "number"]}
-          },
-          "additionalProperties": false
-        }
-      }
+      "oneOf": [{
+        "type": "object",
+        "properties": {
+          "driver": {"type": "string"},
+          "driver_opts": {
+            "type": "object",
+            "patternProperties": {
+              "^.+$": {"type": ["string", "number"]}
+            },
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      }, {
+        "type": "object",
+        "properties": {
+          "external": {
+            "type": ["boolean", "object"],
+            "properties": {
+              "name": {"type": "string"}
+            },
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      }]
     }
   },
   "additionalProperties": false

+ 18 - 1
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_name=data.get('external_name')
                     )
                 )
         return project
@@ -235,6 +237,21 @@ class Project(object):
     def initialize_volumes(self):
         try:
             for volume in self.volumes:
+                if volume.external:
+                    log.debug(
+                        'Volume {0} declared as external. No new '
+                        'volume will be created.'.format(volume.name)
+                    )
+                    if not volume.exists():
+                        raise ConfigurationError(
+                            'Volume {name} declared as external, but could'
+                            ' not be found. Please create the volume manually'
+                            ' using `{command}{name}` and try again.'.format(
+                                name=volume.full_name,
+                                command='docker volume create --name='
+                            )
+                        )
+                    continue
                 volume.create()
         except NotFound:
             raise ConfigurationError(

+ 18 - 1
compose/volume.py

@@ -1,14 +1,18 @@
 from __future__ import absolute_import
 from __future__ import unicode_literals
 
+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_name=None):
         self.client = client
         self.project = project
         self.name = name
         self.driver = driver
         self.driver_opts = driver_opts
+        self.external_name = external_name
 
     def create(self):
         return self.client.create_volume(
@@ -21,6 +25,19 @@ class Volume(object):
     def inspect(self):
         return self.client.inspect_volume(self.full_name)
 
+    def exists(self):
+        try:
+            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)

+ 48 - 2
tests/integration/project_test.py

@@ -4,6 +4,7 @@ from __future__ import unicode_literals
 import random
 
 import py
+from docker.errors import NotFound
 
 from .testcases import DockerClientTestCase
 from compose.config import config
@@ -624,7 +625,7 @@ class ProjectTest(DockerClientTestCase):
         self.assertEqual(volume_data['Name'], full_vol_name)
         self.assertEqual(volume_data['Driver'], 'local')
 
-    def test_project_up_invalid_volume_driver(self):
+    def test_initialize_volumes_invalid_volume_driver(self):
         vol_name = '{0:x}'.format(random.getrandbits(32))
 
         config_data = config.Config(
@@ -642,7 +643,7 @@ class ProjectTest(DockerClientTestCase):
         with self.assertRaises(config.ConfigurationError):
             project.initialize_volumes()
 
-    def test_project_up_updated_driver(self):
+    def test_initialize_volumes_updated_driver(self):
         vol_name = '{0:x}'.format(random.getrandbits(32))
         full_vol_name = 'composetest_{0}'.format(vol_name)
 
@@ -675,3 +676,48 @@ class ProjectTest(DockerClientTestCase):
         assert 'Configuration for volume {0} specifies driver smb'.format(
             vol_name
         ) in str(e.exception)
+
+    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)
+        self.client.create_volume(vol_name)
+        config_data = config.Config(
+            version=2, services=[{
+                'name': 'web',
+                'image': 'busybox:latest',
+                'command': 'top'
+            }], volumes={
+                vol_name: {'external': True, 'external_name': vol_name}
+            }
+        )
+        project = Project.from_config(
+            name='composetest',
+            config_data=config_data, client=self.client
+        )
+        project.initialize_volumes()
+
+        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, 'external_name': vol_name}
+            }
+        )
+        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)

+ 40 - 2
tests/integration/volume_test.py

@@ -18,9 +18,12 @@ 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=None):
+        if external and isinstance(external, bool):
+            external = name
         vol = Volume(
-            self.client, 'composetest', name, driver=driver, driver_opts=opts
+            self.client, 'composetest', name, driver=driver, driver_opts=opts,
+            external_name=external
         )
         self.tmp_volumes.append(vol)
         return vol
@@ -54,3 +57,38 @@ class VolumeTest(DockerClientTestCase):
         vol.remove()
         volumes = self.client.volumes()['Volumes']
         assert len([v for v in volumes if v['Name'] == vol.full_name]) == 0
+
+    def test_external_volume(self):
+        vol = self.create_volume('composetest_volume_ext', 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 = 'composetest_alias01'
+        vol = self.create_volume('volume01', external=alias_name)
+        assert vol.external is True
+        assert vol.full_name == alias_name
+        vol.create()
+        info = vol.inspect()
+        assert info['Name'] == alias_name
+
+    def test_exists(self):
+        vol = self.create_volume('volume01')
+        assert vol.exists() is False
+        vol.create()
+        assert vol.exists() is True
+
+    def test_exists_external(self):
+        vol = self.create_volume('volume01', external=True)
+        assert vol.exists() is False
+        vol.create()
+        assert vol.exists() is True
+
+    def test_exists_external_aliased(self):
+        vol = self.create_volume('volume01', external='composetest_alias01')
+        assert vol.exists() is False
+        vol.create()
+        assert vol.exists() is True

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

@@ -775,6 +775,37 @@ 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'
+
+    def test_external_volume_invalid_config(self):
+        config_details = build_config_details({
+            'version': 2,
+            'services': {
+                'bogus': {'image': 'busybox'}
+            },
+            'volumes': {
+                'ext': {'external': True, 'driver': 'foo'}
+            }
+        })
+        with self.assertRaises(ConfigurationError):
+            config.load(config_details)
+
 
 class PortsTest(unittest.TestCase):
     INVALID_PORTS_TYPES = [