diff --git a/compose/config.py b/compose/config.py index af8983961..aa2db2c71 100644 --- a/compose/config.py +++ b/compose/config.py @@ -83,6 +83,13 @@ SUPPORTED_FILENAMES = [ ] +PATH_START_CHARS = [ + '/', + '.', + '~', +] + + log = logging.getLogger(__name__) @@ -253,7 +260,7 @@ def process_container_options(service_dict, working_dir=None): raise ConfigurationError("Invalid 'memswap_limit' configuration for %s service: when defining 'memswap_limit' you must set 'mem_limit' as well" % service_dict['name']) if 'volumes' in service_dict and service_dict.get('volume_driver') is None: - service_dict['volumes'] = resolve_volume_paths(service_dict['volumes'], working_dir=working_dir) + service_dict['volumes'] = resolve_volume_paths(service_dict, working_dir=working_dir) if 'build' in service_dict: service_dict['build'] = resolve_build_path(service_dict['build'], working_dir=working_dir) @@ -414,18 +421,33 @@ def env_vars_from_file(filename): return env -def resolve_volume_paths(volumes, working_dir=None): +def resolve_volume_paths(service_dict, working_dir=None): if working_dir is None: raise Exception("No working_dir passed to resolve_volume_paths()") - return [resolve_volume_path(v, working_dir) for v in volumes] + return [ + resolve_volume_path(v, working_dir, service_dict['name']) + for v in service_dict['volumes'] + ] -def resolve_volume_path(volume, working_dir): +def resolve_volume_path(volume, working_dir, service_name): container_path, host_path = split_path_mapping(volume) container_path = os.path.expanduser(os.path.expandvars(container_path)) + if host_path is not None: host_path = os.path.expanduser(os.path.expandvars(host_path)) + + if not any(host_path.startswith(c) for c in PATH_START_CHARS): + log.warn( + 'Warning: the mapping "{0}" in the volumes config for ' + 'service "{1}" is ambiguous. In a future version of Docker, ' + 'it will designate a "named" volume ' + '(see https://github.com/docker/docker/pull/14242). ' + 'To prevent unexpected behaviour, change it to "./{0}"' + .format(volume, service_name) + ) + return "%s:%s" % (expand_path(working_dir, host_path), container_path) else: return container_path diff --git a/docs/yml.md b/docs/yml.md index f89d107bd..bd339ec1a 100644 --- a/docs/yml.md +++ b/docs/yml.md @@ -131,11 +131,12 @@ Mount paths as volumes, optionally specifying a path on the host machine volumes: - /var/lib/mysql - - cache/:/tmp/cache + - ./cache:/tmp/cache - ~/configs:/etc/configs/:ro You can mount a relative path on the host, which will expand relative to -the directory of the Compose configuration file being used. +the directory of the Compose configuration file being used. Relative paths +should always begin with `.` or `..`. > Note: No path expansion will be done if you have also specified a > `volume_driver`. diff --git a/tests/unit/config_test.py b/tests/unit/config_test.py index c653ade20..c523798f2 100644 --- a/tests/unit/config_test.py +++ b/tests/unit/config_test.py @@ -72,7 +72,52 @@ class VolumePathTest(unittest.TestCase): d = make_service_dict('foo', {'volumes': ['~:/container/path']}, working_dir='.') self.assertEqual(d['volumes'], ['/home/user:/container/path']) - def test_named_volume_with_driver(self): + @mock.patch.dict(os.environ) + def test_volume_binding_with_local_dir_name_raises_warning(self): + def make_dict(**config): + make_service_dict('foo', config, working_dir='.') + + with mock.patch('compose.config.log.warn') as warn: + make_dict(volumes=['/container/path']) + self.assertEqual(0, warn.call_count) + + make_dict(volumes=['/data:/container/path']) + self.assertEqual(0, warn.call_count) + + make_dict(volumes=['.:/container/path']) + self.assertEqual(0, warn.call_count) + + make_dict(volumes=['..:/container/path']) + self.assertEqual(0, warn.call_count) + + make_dict(volumes=['./data:/container/path']) + self.assertEqual(0, warn.call_count) + + make_dict(volumes=['../data:/container/path']) + self.assertEqual(0, warn.call_count) + + make_dict(volumes=['.profile:/container/path']) + self.assertEqual(0, warn.call_count) + + make_dict(volumes=['~:/container/path']) + self.assertEqual(0, warn.call_count) + + make_dict(volumes=['~/data:/container/path']) + self.assertEqual(0, warn.call_count) + + make_dict(volumes=['~tmp:/container/path']) + self.assertEqual(0, warn.call_count) + + make_dict(volumes=['data:/container/path'], volume_driver='mydriver') + self.assertEqual(0, warn.call_count) + + make_dict(volumes=['data:/container/path']) + self.assertEqual(1, warn.call_count) + warning = warn.call_args[0][0] + self.assertIn('"data:/container/path"', warning) + self.assertIn('"./data:/container/path"', warning) + + def test_named_volume_with_driver_does_not_expand(self): d = make_service_dict('foo', { 'volumes': ['namedvolume:/data'], 'volume_driver': 'foodriver',