Skip to content

Commit df87bd9

Browse files
delbert@umn.eduDanElbert
delbert@umn.edu
authored andcommitted
Added devices configuration option
Signed-off-by: Dan Elbert <dan.elbert@gmail.com>
1 parent 1748b0f commit df87bd9

File tree

6 files changed

+85
-38
lines changed

6 files changed

+85
-38
lines changed

compose/config.py

+23-18
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
'cpuset',
1111
'command',
1212
'detach',
13+
'devices',
1314
'dns',
1415
'dns_search',
1516
'domainname',
@@ -50,6 +51,7 @@
5051
'add_host': 'extra_hosts',
5152
'hosts': 'extra_hosts',
5253
'extra_host': 'extra_hosts',
54+
'device': 'devices',
5355
'link': 'links',
5456
'port': 'ports',
5557
'privilege': 'privileged',
@@ -200,11 +202,14 @@ def merge_service_dicts(base, override):
200202
override.get('environment'),
201203
)
202204

203-
if 'volumes' in base or 'volumes' in override:
204-
d['volumes'] = merge_volumes(
205-
base.get('volumes'),
206-
override.get('volumes'),
207-
)
205+
path_mapping_keys = ['volumes', 'devices']
206+
207+
for key in path_mapping_keys:
208+
if key in base or key in override:
209+
d[key] = merge_path_mappings(
210+
base.get(key),
211+
override.get(key),
212+
)
208213

209214
if 'labels' in base or 'labels' in override:
210215
d['labels'] = merge_labels(
@@ -230,7 +235,7 @@ def merge_service_dicts(base, override):
230235
if key in base or key in override:
231236
d[key] = to_list(base.get(key)) + to_list(override.get(key))
232237

233-
already_merged_keys = ['environment', 'volumes', 'labels'] + list_keys + list_or_string_keys
238+
already_merged_keys = ['environment', 'labels'] + path_mapping_keys + list_keys + list_or_string_keys
234239

235240
for k in set(ALLOWED_KEYS) - set(already_merged_keys):
236241
if k in override:
@@ -346,7 +351,7 @@ def resolve_host_paths(volumes, working_dir=None):
346351

347352

348353
def resolve_host_path(volume, working_dir):
349-
container_path, host_path = split_volume(volume)
354+
container_path, host_path = split_path_mapping(volume)
350355
if host_path is not None:
351356
host_path = os.path.expanduser(host_path)
352357
host_path = os.path.expandvars(host_path)
@@ -368,32 +373,32 @@ def validate_paths(service_dict):
368373
raise ConfigurationError("build path %s either does not exist or is not accessible." % build_path)
369374

370375

371-
def merge_volumes(base, override):
372-
d = dict_from_volumes(base)
373-
d.update(dict_from_volumes(override))
374-
return volumes_from_dict(d)
376+
def merge_path_mappings(base, override):
377+
d = dict_from_path_mappings(base)
378+
d.update(dict_from_path_mappings(override))
379+
return path_mappings_from_dict(d)
375380

376381

377-
def dict_from_volumes(volumes):
378-
if volumes:
379-
return dict(split_volume(v) for v in volumes)
382+
def dict_from_path_mappings(path_mappings):
383+
if path_mappings:
384+
return dict(split_path_mapping(v) for v in path_mappings)
380385
else:
381386
return {}
382387

383388

384-
def volumes_from_dict(d):
385-
return [join_volume(v) for v in d.items()]
389+
def path_mappings_from_dict(d):
390+
return [join_path_mapping(v) for v in d.items()]
386391

387392

388-
def split_volume(string):
393+
def split_path_mapping(string):
389394
if ':' in string:
390395
(host, container) = string.split(':', 1)
391396
return (container, host)
392397
else:
393398
return (string, None)
394399

395400

396-
def join_volume(pair):
401+
def join_path_mapping(pair):
397402
(container, host) = pair
398403
if host is None:
399404
return container

compose/service.py

+4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
DOCKER_START_KEYS = [
2121
'cap_add',
2222
'cap_drop',
23+
'devices',
2324
'dns',
2425
'dns_search',
2526
'env_file',
@@ -441,13 +442,16 @@ def _get_container_host_config(self, override_options, one_off=False):
441442
extra_hosts = build_extra_hosts(options.get('extra_hosts', None))
442443
read_only = options.get('read_only', None)
443444

445+
devices = options.get('devices', None)
446+
444447
return create_host_config(
445448
links=self._get_links(link_to_self=one_off),
446449
port_bindings=port_bindings,
447450
binds=volume_bindings,
448451
volumes_from=options.get('volumes_from'),
449452
privileged=privileged,
450453
network_mode=self._get_net(),
454+
devices=devices,
451455
dns=dns,
452456
dns_search=dns_search,
453457
restart_policy=restart,

docs/extends.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -342,8 +342,8 @@ environment:
342342
- BAZ=local
343343
```
344344

345-
Finally, for `volumes`, Compose "merges" entries together with locally-defined
346-
bindings taking precedence:
345+
Finally, for `volumes` and `devices`, Compose "merges" entries together with
346+
locally-defined bindings taking precedence:
347347

348348
```yaml
349349
# original service
@@ -361,4 +361,4 @@ volumes:
361361
- /original-dir/foo:/foo
362362
- /local-dir/bar:/bar
363363
- /local-dir/baz/:baz
364-
```
364+
```

docs/yml.md

+12-2
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ image: a4bc65fd
2929

3030
### build
3131

32-
Path to a directory containing a Dockerfile. When the value supplied is a
33-
relative path, it is interpreted as relative to the location of the yml file
32+
Path to a directory containing a Dockerfile. When the value supplied is a
33+
relative path, it is interpreted as relative to the location of the yml file
3434
itself. This directory is also the build context that is sent to the Docker daemon.
3535

3636
Compose will build and tag it with a generated name, and use that image thereafter.
@@ -342,6 +342,16 @@ dns_search:
342342
- dc2.example.com
343343
```
344344

345+
### devices
346+
347+
List of device mappings. Uses the same format as the `--device` docker
348+
client create option.
349+
350+
```
351+
devices:
352+
- "/dev/ttyUSB0:/dev/ttyUSB0"
353+
```
354+
345355
### working\_dir, entrypoint, user, hostname, domainname, mem\_limit, privileged, restart, stdin\_open, tty, cpu\_shares, cpuset, read\_only
346356

347357
Each of these is a single value, analogous to its

tests/integration/service_test.py

+13
Original file line numberDiff line numberDiff line change
@@ -669,3 +669,16 @@ def test_log_drive_none(self):
669669

670670
self.assertEqual('none', log_config['Type'])
671671
self.assertFalse(log_config['Config'])
672+
673+
def test_devices(self):
674+
service = self.create_service('web', devices=["/dev/random:/dev/mapped-random"])
675+
device_config = create_and_start_container(service).get('HostConfig.Devices')
676+
677+
device_dict = {
678+
'PathOnHost': '/dev/random',
679+
'CgroupPermissions': 'rwm',
680+
'PathInContainer': '/dev/mapped-random'
681+
}
682+
683+
self.assertEqual(1, len(device_config))
684+
self.assertDictEqual(device_dict, device_config[0])

tests/unit/config_test.py

+30-15
Original file line numberDiff line numberDiff line change
@@ -54,46 +54,61 @@ def test_volume_binding_with_home(self):
5454
self.assertEqual(d['volumes'], ['/home/user:/container/path'])
5555

5656

57-
class MergeVolumesTest(unittest.TestCase):
57+
class MergePathMappingTest(object):
58+
def config_name(self):
59+
return ""
60+
5861
def test_empty(self):
5962
service_dict = config.merge_service_dicts({}, {})
60-
self.assertNotIn('volumes', service_dict)
63+
self.assertNotIn(self.config_name(), service_dict)
6164

6265
def test_no_override(self):
6366
service_dict = config.merge_service_dicts(
64-
{'volumes': ['/foo:/code', '/data']},
67+
{self.config_name(): ['/foo:/code', '/data']},
6568
{},
6669
)
67-
self.assertEqual(set(service_dict['volumes']), set(['/foo:/code', '/data']))
70+
self.assertEqual(set(service_dict[self.config_name()]), set(['/foo:/code', '/data']))
6871

6972
def test_no_base(self):
7073
service_dict = config.merge_service_dicts(
7174
{},
72-
{'volumes': ['/bar:/code']},
75+
{self.config_name(): ['/bar:/code']},
7376
)
74-
self.assertEqual(set(service_dict['volumes']), set(['/bar:/code']))
77+
self.assertEqual(set(service_dict[self.config_name()]), set(['/bar:/code']))
7578

7679
def test_override_explicit_path(self):
7780
service_dict = config.merge_service_dicts(
78-
{'volumes': ['/foo:/code', '/data']},
79-
{'volumes': ['/bar:/code']},
81+
{self.config_name(): ['/foo:/code', '/data']},
82+
{self.config_name(): ['/bar:/code']},
8083
)
81-
self.assertEqual(set(service_dict['volumes']), set(['/bar:/code', '/data']))
84+
self.assertEqual(set(service_dict[self.config_name()]), set(['/bar:/code', '/data']))
8285

8386
def test_add_explicit_path(self):
8487
service_dict = config.merge_service_dicts(
85-
{'volumes': ['/foo:/code', '/data']},
86-
{'volumes': ['/bar:/code', '/quux:/data']},
88+
{self.config_name(): ['/foo:/code', '/data']},
89+
{self.config_name(): ['/bar:/code', '/quux:/data']},
8790
)
88-
self.assertEqual(set(service_dict['volumes']), set(['/bar:/code', '/quux:/data']))
91+
self.assertEqual(set(service_dict[self.config_name()]), set(['/bar:/code', '/quux:/data']))
8992

9093
def test_remove_explicit_path(self):
9194
service_dict = config.merge_service_dicts(
92-
{'volumes': ['/foo:/code', '/quux:/data']},
93-
{'volumes': ['/bar:/code', '/data']},
95+
{self.config_name(): ['/foo:/code', '/quux:/data']},
96+
{self.config_name(): ['/bar:/code', '/data']},
9497
)
95-
self.assertEqual(set(service_dict['volumes']), set(['/bar:/code', '/data']))
98+
self.assertEqual(set(service_dict[self.config_name()]), set(['/bar:/code', '/data']))
99+
100+
101+
class MergeVolumesTest(unittest.TestCase, MergePathMappingTest):
102+
def config_name(self):
103+
return 'volumes'
104+
105+
106+
class MergeDevicesTest(unittest.TestCase, MergePathMappingTest):
107+
def config_name(self):
108+
return 'devices'
109+
96110

111+
class BuildOrImageMergeTest(unittest.TestCase):
97112
def test_merge_build_or_image_no_override(self):
98113
self.assertEqual(
99114
config.merge_service_dicts({'build': '.'}, {}),

0 commit comments

Comments
 (0)