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 <joffrey@docker.com>
This commit is contained in:
Joffrey F 2016-01-12 16:53:49 -08:00
parent 05935b5e54
commit 9cb58b796e
6 changed files with 93 additions and 21 deletions

View File

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

View File

@ -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:

View File

@ -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 exists(self):
def is_user_created(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)

View File

@ -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)

View File

@ -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): def test_external_volume(self):
vol = Volume(self.client, 'composetest', 'uservolume01') vol = self.create_volume('volume01', external=True)
try: assert vol.external is True
self.client.create_volume('uservolume01') assert vol.full_name == vol.name
assert vol.is_user_created is True vol.create()
finally: info = vol.inspect()
self.client.remove_volume('uservolume01') assert info['Name'] == vol.name
vol2 = Volume(self.client, 'composetest', 'volume01')
assert vol2.is_user_created is False 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

View File

@ -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 = [