mirror of
https://github.com/docker/compose.git
synced 2025-07-23 13:45:00 +02:00
Merge pull request #1400 from DanElbert/754-device_option
Added devices config handling and device HostConfig handling
This commit is contained in:
commit
4997facbb4
@ -10,6 +10,7 @@ DOCKER_CONFIG_KEYS = [
|
|||||||
'cpuset',
|
'cpuset',
|
||||||
'command',
|
'command',
|
||||||
'detach',
|
'detach',
|
||||||
|
'devices',
|
||||||
'dns',
|
'dns',
|
||||||
'dns_search',
|
'dns_search',
|
||||||
'domainname',
|
'domainname',
|
||||||
@ -50,6 +51,7 @@ DOCKER_CONFIG_HINTS = {
|
|||||||
'add_host': 'extra_hosts',
|
'add_host': 'extra_hosts',
|
||||||
'hosts': 'extra_hosts',
|
'hosts': 'extra_hosts',
|
||||||
'extra_host': 'extra_hosts',
|
'extra_host': 'extra_hosts',
|
||||||
|
'device': 'devices',
|
||||||
'link': 'links',
|
'link': 'links',
|
||||||
'port': 'ports',
|
'port': 'ports',
|
||||||
'privilege': 'privileged',
|
'privilege': 'privileged',
|
||||||
@ -200,11 +202,14 @@ def merge_service_dicts(base, override):
|
|||||||
override.get('environment'),
|
override.get('environment'),
|
||||||
)
|
)
|
||||||
|
|
||||||
if 'volumes' in base or 'volumes' in override:
|
path_mapping_keys = ['volumes', 'devices']
|
||||||
d['volumes'] = merge_volumes(
|
|
||||||
base.get('volumes'),
|
for key in path_mapping_keys:
|
||||||
override.get('volumes'),
|
if key in base or key in override:
|
||||||
)
|
d[key] = merge_path_mappings(
|
||||||
|
base.get(key),
|
||||||
|
override.get(key),
|
||||||
|
)
|
||||||
|
|
||||||
if 'labels' in base or 'labels' in override:
|
if 'labels' in base or 'labels' in override:
|
||||||
d['labels'] = merge_labels(
|
d['labels'] = merge_labels(
|
||||||
@ -230,7 +235,7 @@ def merge_service_dicts(base, override):
|
|||||||
if key in base or key in override:
|
if key in base or key in override:
|
||||||
d[key] = to_list(base.get(key)) + to_list(override.get(key))
|
d[key] = to_list(base.get(key)) + to_list(override.get(key))
|
||||||
|
|
||||||
already_merged_keys = ['environment', 'volumes', 'labels'] + list_keys + list_or_string_keys
|
already_merged_keys = ['environment', 'labels'] + path_mapping_keys + list_keys + list_or_string_keys
|
||||||
|
|
||||||
for k in set(ALLOWED_KEYS) - set(already_merged_keys):
|
for k in set(ALLOWED_KEYS) - set(already_merged_keys):
|
||||||
if k in override:
|
if k in override:
|
||||||
@ -346,7 +351,7 @@ def resolve_host_paths(volumes, working_dir=None):
|
|||||||
|
|
||||||
|
|
||||||
def resolve_host_path(volume, working_dir):
|
def resolve_host_path(volume, working_dir):
|
||||||
container_path, host_path = split_volume(volume)
|
container_path, host_path = split_path_mapping(volume)
|
||||||
if host_path is not None:
|
if host_path is not None:
|
||||||
host_path = os.path.expanduser(host_path)
|
host_path = os.path.expanduser(host_path)
|
||||||
host_path = os.path.expandvars(host_path)
|
host_path = os.path.expandvars(host_path)
|
||||||
@ -368,24 +373,24 @@ def validate_paths(service_dict):
|
|||||||
raise ConfigurationError("build path %s either does not exist or is not accessible." % build_path)
|
raise ConfigurationError("build path %s either does not exist or is not accessible." % build_path)
|
||||||
|
|
||||||
|
|
||||||
def merge_volumes(base, override):
|
def merge_path_mappings(base, override):
|
||||||
d = dict_from_volumes(base)
|
d = dict_from_path_mappings(base)
|
||||||
d.update(dict_from_volumes(override))
|
d.update(dict_from_path_mappings(override))
|
||||||
return volumes_from_dict(d)
|
return path_mappings_from_dict(d)
|
||||||
|
|
||||||
|
|
||||||
def dict_from_volumes(volumes):
|
def dict_from_path_mappings(path_mappings):
|
||||||
if volumes:
|
if path_mappings:
|
||||||
return dict(split_volume(v) for v in volumes)
|
return dict(split_path_mapping(v) for v in path_mappings)
|
||||||
else:
|
else:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def volumes_from_dict(d):
|
def path_mappings_from_dict(d):
|
||||||
return [join_volume(v) for v in d.items()]
|
return [join_path_mapping(v) for v in d.items()]
|
||||||
|
|
||||||
|
|
||||||
def split_volume(string):
|
def split_path_mapping(string):
|
||||||
if ':' in string:
|
if ':' in string:
|
||||||
(host, container) = string.split(':', 1)
|
(host, container) = string.split(':', 1)
|
||||||
return (container, host)
|
return (container, host)
|
||||||
@ -393,7 +398,7 @@ def split_volume(string):
|
|||||||
return (string, None)
|
return (string, None)
|
||||||
|
|
||||||
|
|
||||||
def join_volume(pair):
|
def join_path_mapping(pair):
|
||||||
(container, host) = pair
|
(container, host) = pair
|
||||||
if host is None:
|
if host is None:
|
||||||
return container
|
return container
|
||||||
|
@ -20,6 +20,7 @@ log = logging.getLogger(__name__)
|
|||||||
DOCKER_START_KEYS = [
|
DOCKER_START_KEYS = [
|
||||||
'cap_add',
|
'cap_add',
|
||||||
'cap_drop',
|
'cap_drop',
|
||||||
|
'devices',
|
||||||
'dns',
|
'dns',
|
||||||
'dns_search',
|
'dns_search',
|
||||||
'env_file',
|
'env_file',
|
||||||
@ -441,6 +442,8 @@ class Service(object):
|
|||||||
extra_hosts = build_extra_hosts(options.get('extra_hosts', None))
|
extra_hosts = build_extra_hosts(options.get('extra_hosts', None))
|
||||||
read_only = options.get('read_only', None)
|
read_only = options.get('read_only', None)
|
||||||
|
|
||||||
|
devices = options.get('devices', None)
|
||||||
|
|
||||||
return create_host_config(
|
return create_host_config(
|
||||||
links=self._get_links(link_to_self=one_off),
|
links=self._get_links(link_to_self=one_off),
|
||||||
port_bindings=port_bindings,
|
port_bindings=port_bindings,
|
||||||
@ -448,6 +451,7 @@ class Service(object):
|
|||||||
volumes_from=options.get('volumes_from'),
|
volumes_from=options.get('volumes_from'),
|
||||||
privileged=privileged,
|
privileged=privileged,
|
||||||
network_mode=self._get_net(),
|
network_mode=self._get_net(),
|
||||||
|
devices=devices,
|
||||||
dns=dns,
|
dns=dns,
|
||||||
dns_search=dns_search,
|
dns_search=dns_search,
|
||||||
restart_policy=restart,
|
restart_policy=restart,
|
||||||
|
@ -342,8 +342,8 @@ environment:
|
|||||||
- BAZ=local
|
- BAZ=local
|
||||||
```
|
```
|
||||||
|
|
||||||
Finally, for `volumes`, Compose "merges" entries together with locally-defined
|
Finally, for `volumes` and `devices`, Compose "merges" entries together with
|
||||||
bindings taking precedence:
|
locally-defined bindings taking precedence:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# original service
|
# original service
|
||||||
|
10
docs/yml.md
10
docs/yml.md
@ -342,6 +342,16 @@ dns_search:
|
|||||||
- dc2.example.com
|
- dc2.example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### devices
|
||||||
|
|
||||||
|
List of device mappings. Uses the same format as the `--device` docker
|
||||||
|
client create option.
|
||||||
|
|
||||||
|
```
|
||||||
|
devices:
|
||||||
|
- "/dev/ttyUSB0:/dev/ttyUSB0"
|
||||||
|
```
|
||||||
|
|
||||||
### working\_dir, entrypoint, user, hostname, domainname, mem\_limit, privileged, restart, stdin\_open, tty, cpu\_shares, cpuset, read\_only
|
### working\_dir, entrypoint, user, hostname, domainname, mem\_limit, privileged, restart, stdin\_open, tty, cpu\_shares, cpuset, read\_only
|
||||||
|
|
||||||
Each of these is a single value, analogous to its
|
Each of these is a single value, analogous to its
|
||||||
|
@ -669,3 +669,16 @@ class ServiceTest(DockerClientTestCase):
|
|||||||
|
|
||||||
self.assertEqual('none', log_config['Type'])
|
self.assertEqual('none', log_config['Type'])
|
||||||
self.assertFalse(log_config['Config'])
|
self.assertFalse(log_config['Config'])
|
||||||
|
|
||||||
|
def test_devices(self):
|
||||||
|
service = self.create_service('web', devices=["/dev/random:/dev/mapped-random"])
|
||||||
|
device_config = create_and_start_container(service).get('HostConfig.Devices')
|
||||||
|
|
||||||
|
device_dict = {
|
||||||
|
'PathOnHost': '/dev/random',
|
||||||
|
'CgroupPermissions': 'rwm',
|
||||||
|
'PathInContainer': '/dev/mapped-random'
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assertEqual(1, len(device_config))
|
||||||
|
self.assertDictEqual(device_dict, device_config[0])
|
||||||
|
@ -54,46 +54,61 @@ class VolumePathTest(unittest.TestCase):
|
|||||||
self.assertEqual(d['volumes'], ['/home/user:/container/path'])
|
self.assertEqual(d['volumes'], ['/home/user:/container/path'])
|
||||||
|
|
||||||
|
|
||||||
class MergeVolumesTest(unittest.TestCase):
|
class MergePathMappingTest(object):
|
||||||
|
def config_name(self):
|
||||||
|
return ""
|
||||||
|
|
||||||
def test_empty(self):
|
def test_empty(self):
|
||||||
service_dict = config.merge_service_dicts({}, {})
|
service_dict = config.merge_service_dicts({}, {})
|
||||||
self.assertNotIn('volumes', service_dict)
|
self.assertNotIn(self.config_name(), service_dict)
|
||||||
|
|
||||||
def test_no_override(self):
|
def test_no_override(self):
|
||||||
service_dict = config.merge_service_dicts(
|
service_dict = config.merge_service_dicts(
|
||||||
{'volumes': ['/foo:/code', '/data']},
|
{self.config_name(): ['/foo:/code', '/data']},
|
||||||
{},
|
{},
|
||||||
)
|
)
|
||||||
self.assertEqual(set(service_dict['volumes']), set(['/foo:/code', '/data']))
|
self.assertEqual(set(service_dict[self.config_name()]), set(['/foo:/code', '/data']))
|
||||||
|
|
||||||
def test_no_base(self):
|
def test_no_base(self):
|
||||||
service_dict = config.merge_service_dicts(
|
service_dict = config.merge_service_dicts(
|
||||||
{},
|
{},
|
||||||
{'volumes': ['/bar:/code']},
|
{self.config_name(): ['/bar:/code']},
|
||||||
)
|
)
|
||||||
self.assertEqual(set(service_dict['volumes']), set(['/bar:/code']))
|
self.assertEqual(set(service_dict[self.config_name()]), set(['/bar:/code']))
|
||||||
|
|
||||||
def test_override_explicit_path(self):
|
def test_override_explicit_path(self):
|
||||||
service_dict = config.merge_service_dicts(
|
service_dict = config.merge_service_dicts(
|
||||||
{'volumes': ['/foo:/code', '/data']},
|
{self.config_name(): ['/foo:/code', '/data']},
|
||||||
{'volumes': ['/bar:/code']},
|
{self.config_name(): ['/bar:/code']},
|
||||||
)
|
)
|
||||||
self.assertEqual(set(service_dict['volumes']), set(['/bar:/code', '/data']))
|
self.assertEqual(set(service_dict[self.config_name()]), set(['/bar:/code', '/data']))
|
||||||
|
|
||||||
def test_add_explicit_path(self):
|
def test_add_explicit_path(self):
|
||||||
service_dict = config.merge_service_dicts(
|
service_dict = config.merge_service_dicts(
|
||||||
{'volumes': ['/foo:/code', '/data']},
|
{self.config_name(): ['/foo:/code', '/data']},
|
||||||
{'volumes': ['/bar:/code', '/quux:/data']},
|
{self.config_name(): ['/bar:/code', '/quux:/data']},
|
||||||
)
|
)
|
||||||
self.assertEqual(set(service_dict['volumes']), set(['/bar:/code', '/quux:/data']))
|
self.assertEqual(set(service_dict[self.config_name()]), set(['/bar:/code', '/quux:/data']))
|
||||||
|
|
||||||
def test_remove_explicit_path(self):
|
def test_remove_explicit_path(self):
|
||||||
service_dict = config.merge_service_dicts(
|
service_dict = config.merge_service_dicts(
|
||||||
{'volumes': ['/foo:/code', '/quux:/data']},
|
{self.config_name(): ['/foo:/code', '/quux:/data']},
|
||||||
{'volumes': ['/bar:/code', '/data']},
|
{self.config_name(): ['/bar:/code', '/data']},
|
||||||
)
|
)
|
||||||
self.assertEqual(set(service_dict['volumes']), set(['/bar:/code', '/data']))
|
self.assertEqual(set(service_dict[self.config_name()]), set(['/bar:/code', '/data']))
|
||||||
|
|
||||||
|
|
||||||
|
class MergeVolumesTest(unittest.TestCase, MergePathMappingTest):
|
||||||
|
def config_name(self):
|
||||||
|
return 'volumes'
|
||||||
|
|
||||||
|
|
||||||
|
class MergeDevicesTest(unittest.TestCase, MergePathMappingTest):
|
||||||
|
def config_name(self):
|
||||||
|
return 'devices'
|
||||||
|
|
||||||
|
|
||||||
|
class BuildOrImageMergeTest(unittest.TestCase):
|
||||||
def test_merge_build_or_image_no_override(self):
|
def test_merge_build_or_image_no_override(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
config.merge_service_dicts({'build': '.'}, {}),
|
config.merge_service_dicts({'build': '.'}, {}),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user