mirror of
https://github.com/docker/compose.git
synced 2025-07-21 12:44:54 +02:00
Merge pull request #2647 from shin-/preexisting_volume_detection_rb1
Support for external volumes
This commit is contained in:
commit
153185eadb
@ -273,6 +273,13 @@ def load_volumes(config_files):
|
|||||||
for config_file in config_files:
|
for config_file in config_files:
|
||||||
for name, volume_config in config_file.config.get('volumes', {}).items():
|
for name, volume_config in config_file.config.get('volumes', {}).items():
|
||||||
volumes.update({name: volume_config})
|
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
|
return volumes
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
"definitions": {
|
"definitions": {
|
||||||
"volume": {
|
"volume": {
|
||||||
"id": "#/definitions/volume",
|
"id": "#/definitions/volume",
|
||||||
|
"oneOf": [{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"driver": {"type": "string"},
|
"driver": {"type": "string"},
|
||||||
@ -42,7 +43,21 @@
|
|||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}, {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"external": {
|
||||||
|
"type": ["boolean", "object"],
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
@ -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_name=data.get('external_name')
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return project
|
return project
|
||||||
@ -235,6 +237,21 @@ 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.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()
|
volume.create()
|
||||||
except NotFound:
|
except NotFound:
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
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_name=None):
|
||||||
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 = external_name
|
||||||
|
|
||||||
def create(self):
|
def create(self):
|
||||||
return self.client.create_volume(
|
return self.client.create_volume(
|
||||||
@ -21,6 +25,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)
|
||||||
|
|
||||||
|
def exists(self):
|
||||||
|
try:
|
||||||
|
self.inspect()
|
||||||
|
except NotFound:
|
||||||
|
return False
|
||||||
|
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)
|
||||||
|
@ -4,6 +4,7 @@ from __future__ import unicode_literals
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
import py
|
import py
|
||||||
|
from docker.errors import NotFound
|
||||||
|
|
||||||
from .testcases import DockerClientTestCase
|
from .testcases import DockerClientTestCase
|
||||||
from compose.config import config
|
from compose.config import config
|
||||||
@ -624,7 +625,7 @@ class ProjectTest(DockerClientTestCase):
|
|||||||
self.assertEqual(volume_data['Name'], full_vol_name)
|
self.assertEqual(volume_data['Name'], full_vol_name)
|
||||||
self.assertEqual(volume_data['Driver'], 'local')
|
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))
|
vol_name = '{0:x}'.format(random.getrandbits(32))
|
||||||
|
|
||||||
config_data = config.Config(
|
config_data = config.Config(
|
||||||
@ -642,7 +643,7 @@ class ProjectTest(DockerClientTestCase):
|
|||||||
with self.assertRaises(config.ConfigurationError):
|
with self.assertRaises(config.ConfigurationError):
|
||||||
project.initialize_volumes()
|
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))
|
vol_name = '{0:x}'.format(random.getrandbits(32))
|
||||||
full_vol_name = 'composetest_{0}'.format(vol_name)
|
full_vol_name = 'composetest_{0}'.format(vol_name)
|
||||||
|
|
||||||
@ -675,3 +676,48 @@ class ProjectTest(DockerClientTestCase):
|
|||||||
assert 'Configuration for volume {0} specifies driver smb'.format(
|
assert 'Configuration for volume {0} specifies driver smb'.format(
|
||||||
vol_name
|
vol_name
|
||||||
) in str(e.exception)
|
) 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)
|
||||||
|
@ -18,9 +18,12 @@ 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=None):
|
||||||
|
if external and isinstance(external, bool):
|
||||||
|
external = name
|
||||||
vol = Volume(
|
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)
|
self.tmp_volumes.append(vol)
|
||||||
return vol
|
return vol
|
||||||
@ -54,3 +57,38 @@ class VolumeTest(DockerClientTestCase):
|
|||||||
vol.remove()
|
vol.remove()
|
||||||
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_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
|
||||||
|
@ -775,6 +775,37 @@ 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'
|
||||||
|
|
||||||
|
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):
|
class PortsTest(unittest.TestCase):
|
||||||
INVALID_PORTS_TYPES = [
|
INVALID_PORTS_TYPES = [
|
||||||
|
Loading…
x
Reference in New Issue
Block a user