Skip to content

Commit

Permalink
Add support for mount syntax
Browse files Browse the repository at this point in the history
Signed-off-by: Joffrey F <joffrey@docker.com>
  • Loading branch information
shin- committed Dec 5, 2017
1 parent 6cd0bc4 commit 3797e8a
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 34 deletions.
40 changes: 22 additions & 18 deletions compose/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from .sort_services import get_container_name_from_network_mode
from .sort_services import get_service_name_from_network_mode
from .sort_services import sort_service_dicts
from .types import MountSpec
from .types import parse_extra_hosts
from .types import parse_restart_spec
from .types import ServiceLink
Expand Down Expand Up @@ -809,6 +810,20 @@ def process_healthcheck(service_dict):
return service_dict


def finalize_service_volumes(service_dict, environment):
if 'volumes' in service_dict:
finalized_volumes = []
normalize = environment.get_boolean('COMPOSE_CONVERT_WINDOWS_PATHS')
for v in service_dict['volumes']:
if isinstance(v, dict):
finalized_volumes.append(MountSpec.parse(v, normalize))
else:
finalized_volumes.append(VolumeSpec.parse(v, normalize))
service_dict['volumes'] = finalized_volumes

return service_dict


def finalize_service(service_config, service_names, version, environment):
service_dict = dict(service_config.config)

Expand All @@ -822,12 +837,7 @@ def finalize_service(service_config, service_names, version, environment):
for vf in service_dict['volumes_from']
]

if 'volumes' in service_dict:
service_dict['volumes'] = [
VolumeSpec.parse(
v, environment.get_boolean('COMPOSE_CONVERT_WINDOWS_PATHS')
) for v in service_dict['volumes']
]
service_dict = finalize_service_volumes(service_dict, environment)

if 'net' in service_dict:
network_mode = service_dict.pop('net')
Expand Down Expand Up @@ -1143,19 +1153,13 @@ def resolve_volume_paths(working_dir, service_dict):


def resolve_volume_path(working_dir, volume):
mount_params = None
if isinstance(volume, dict):
container_path = volume.get('target')
host_path = volume.get('source')
mode = None
if host_path:
if volume.get('read_only'):
mode = 'ro'
if volume.get('volume', {}).get('nocopy'):
mode = 'nocopy'
mount_params = (host_path, mode)
else:
container_path, mount_params = split_path_mapping(volume)
if volume.get('source', '').startswith('.') and volume['type'] == 'mount':
volume['source'] = expand_path(working_dir, volume['source'])
return volume

mount_params = None
container_path, mount_params = split_path_mapping(volume)

if mount_params is not None:
host_path, mode = mount_params
Expand Down
34 changes: 33 additions & 1 deletion compose/config/config_schema_v2.3.json
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,39 @@
},
"user": {"type": "string"},
"userns_mode": {"type": "string"},
"volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"volumes": {
"type": "array",
"items": {
"oneOf": [
{"type": "string"},
{
"type": "object",
"required": ["type"],
"additionalProperties": false,
"properties": {
"type": {"type": "string"},
"source": {"type": "string"},
"target": {"type": "string"},
"read_only": {"type": "boolean"},
"consistency": {"type": "string"},
"bind": {
"type": "object",
"properties": {
"propagation": {"type": "string"}
}
},
"volume": {
"type": "object",
"properties": {
"nocopy": {"type": "boolean"}
}
}
}
}
],
"uniqueItems": true
}
},
"volume_driver": {"type": "string"},
"volumes_from": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"working_dir": {"type": "string"}
Expand Down
6 changes: 6 additions & 0 deletions compose/config/serialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from compose.config import types
from compose.const import COMPOSEFILE_V1 as V1
from compose.const import COMPOSEFILE_V2_1 as V2_1
from compose.const import COMPOSEFILE_V2_3 as V2_3
from compose.const import COMPOSEFILE_V3_0 as V3_0
from compose.const import COMPOSEFILE_V3_2 as V3_2
from compose.const import COMPOSEFILE_V3_4 as V3_4
Expand Down Expand Up @@ -34,6 +35,7 @@ def serialize_string(dumper, data):
return representer(data)


yaml.SafeDumper.add_representer(types.MountSpec, serialize_dict_type)
yaml.SafeDumper.add_representer(types.VolumeFromSpec, serialize_config_type)
yaml.SafeDumper.add_representer(types.VolumeSpec, serialize_config_type)
yaml.SafeDumper.add_representer(types.ServiceSecret, serialize_dict_type)
Expand Down Expand Up @@ -140,5 +142,9 @@ def denormalize_service_dict(service_dict, version, image_digest=None):
p.legacy_repr() if isinstance(p, types.ServicePort) else p
for p in service_dict['ports']
]
if 'volumes' in service_dict and (version < V2_3 or (version > V3_0 and version < V3_2)):
service_dict['volumes'] = [
v.legacy_repr() if isinstance(v, types.MountSpec) else v for v in service_dict['volumes']
]

return service_dict
13 changes: 13 additions & 0 deletions compose/config/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,15 @@ class MountSpec(object):
}
_fields = ['type', 'source', 'target', 'read_only', 'consistency']

@classmethod
def parse(cls, mount_dict, normalize=False):
if mount_dict.get('source'):
mount_dict['source'] = os.path.normpath(mount_dict['source'])
if normalize:
mount_dict['source'] = normalize_path_for_engine(mount_dict['source'])

return cls(**mount_dict)

def __init__(self, type, source=None, target=None, read_only=None, consistency=None, **kwargs):
self.type = type
self.source = source
Expand Down Expand Up @@ -174,6 +183,10 @@ def repr(self):
def is_named_volume(self):
return self.type == 'volume' and self.source

@property
def external(self):
return self.source


class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')):

Expand Down
12 changes: 9 additions & 3 deletions compose/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -792,8 +792,13 @@ def _get_container_create_options(
override_options['binds'] = binds
container_options['environment'].update(affinity)

container_options['volumes'] = dict(
(v.internal, {}) for v in container_options.get('volumes') or {})
if 'volumes' in container_options:
container_volumes = [
v for v in container_options.get('volumes') if isinstance(v, VolumeSpec)
]
container_mounts = [v for v in container_options.get('volumes') if isinstance(v, MountSpec)]
container_options['volumes'] = dict((v.internal, {}) for v in container_volumes or {})
override_options['mounts'] = [build_mount(v) for v in container_mounts] or None

secret_volumes = self.get_secret_volumes()
if secret_volumes:
Expand All @@ -803,7 +808,8 @@ def _get_container_create_options(
(v.target, {}) for v in secret_volumes
)
else:
override_options['mounts'] = [build_mount(v) for v in secret_volumes]
override_options['mounts'] = override_options.get('mounts') or []
override_options['mounts'].extend([build_mount(v) for v in secret_volumes])

container_options['image'] = self.image_name

Expand Down
9 changes: 7 additions & 2 deletions compose/volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from docker.utils import version_lt

from .config import ConfigurationError
from .config.types import VolumeSpec
from .const import LABEL_PROJECT
from .const import LABEL_VOLUME

Expand Down Expand Up @@ -145,5 +146,9 @@ def namespace_spec(self, volume_spec):
if not volume_spec.is_named_volume:
return volume_spec

volume = self.volumes[volume_spec.external]
return volume_spec._replace(external=volume.full_name)
if isinstance(volume_spec, VolumeSpec):
volume = self.volumes[volume_spec.external]
return volume_spec._replace(external=volume.full_name)
else:
volume_spec.source = self.volumes[volume_spec.source].full_name
return volume_spec
22 changes: 15 additions & 7 deletions tests/acceptance/cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,13 +428,21 @@ def test_config_v3(self):
'timeout': '1s',
'retries': 5,
},
'volumes': [
'/host/path:/container/path:ro',
'foobar:/container/volumepath:rw',
'/anonymous',
'foobar:/container/volumepath2:nocopy'
],

'volumes': [{
'read_only': True,
'source': '/host/path',
'target': '/container/path',
'type': 'bind'
}, {
'source': 'foobar', 'target': '/container/volumepath', 'type': 'volume'
}, {
'target': '/anonymous', 'type': 'volume'
}, {
'source': 'foobar',
'target': '/container/volumepath2',
'type': 'volume',
'volume': {'nocopy': True}
}],
'stop_grace_period': '20s',
},
},
Expand Down
9 changes: 6 additions & 3 deletions tests/unit/config/config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1137,9 +1137,12 @@ def test_load_with_multiple_files_v3_2(self):
details = config.ConfigDetails('.', [base_file, override_file])
service_dicts = config.load(details).services
svc_volumes = map(lambda v: v.repr(), service_dicts[0]['volumes'])
assert sorted(svc_volumes) == sorted(
['/anonymous', '/c:/b:rw', 'vol:/x:ro']
)
for vol in svc_volumes:
assert vol in [
'/anonymous',
'/c:/b:rw',
{'source': 'vol', 'target': '/x', 'type': 'volume', 'read_only': True}
]

@mock.patch.dict(os.environ)
def test_volume_mode_override(self):
Expand Down

0 comments on commit 3797e8a

Please sign in to comment.