') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Added devices configuration option · docker/compose@df87bd9 · GitHub
Skip to content

Commit df87bd9

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

6 files changed

Lines changed: 85 additions & 38 deletions

File tree

‎compose/config.py‎

Lines changed: 23 additions & 18 deletions
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'inbaseor'volumes'inoverride:
204-
d['volumes'] =merge_volumes(
205-
base.get('volumes'),
206-
override.get('volumes'),
207-
)
205+
path_mapping_keys= ['volumes', 'devices']
206+
207+
forkeyinpath_mapping_keys:
208+
ifkeyinbaseorkeyinoverride:
209+
d[key] =merge_path_mappings(
210+
base.get(key),
211+
override.get(key),
212+
)
208213

209214
if'labels'inbaseor'labels'inoverride:
210215
d['labels'] =merge_labels(
@@ -230,7 +235,7 @@ def merge_service_dicts(base, override):
230235
ifkeyinbaseorkeyinoverride:
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
forkinset(ALLOWED_KEYS) -set(already_merged_keys):
236241
ifkinoverride:
@@ -346,7 +351,7 @@ def resolve_host_paths(volumes, working_dir=None):
346351

347352

348353
defresolve_host_path(volume, working_dir):
349-
container_path, host_path=split_volume(volume)
354+
container_path, host_path=split_path_mapping(volume)
350355
ifhost_pathisnotNone:
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
raiseConfigurationError("build path %s either does not exist or is not accessible."%build_path)
369374

370375

371-
defmerge_volumes(base, override):
372-
d=dict_from_volumes(base)
373-
d.update(dict_from_volumes(override))
374-
returnvolumes_from_dict(d)
376+
defmerge_path_mappings(base, override):
377+
d=dict_from_path_mappings(base)
378+
d.update(dict_from_path_mappings(override))
379+
returnpath_mappings_from_dict(d)
375380

376381

377-
defdict_from_volumes(volumes):
378-
ifvolumes:
379-
returndict(split_volume(v) forvinvolumes)
382+
defdict_from_path_mappings(path_mappings):
383+
ifpath_mappings:
384+
returndict(split_path_mapping(v) forvinpath_mappings)
380385
else:
381386
return {}
382387

383388

384-
defvolumes_from_dict(d):
385-
return [join_volume(v) forvind.items()]
389+
defpath_mappings_from_dict(d):
390+
return [join_path_mapping(v) forvind.items()]
386391

387392

388-
defsplit_volume(string):
393+
defsplit_path_mapping(string):
389394
if':'instring:
390395
(host, container) =string.split(':', 1)
391396
return (container, host)
392397
else:
393398
return (string, None)
394399

395400

396-
defjoin_volume(pair):
401+
defjoin_path_mapping(pair):
397402
(container, host) =pair
398403
ifhostisNone:
399404
returncontainer

‎compose/service.py‎

Lines changed: 4 additions & 0 deletions
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
returncreate_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‎

Lines changed: 3 additions & 3 deletions
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‎

Lines changed: 12 additions & 2 deletions
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‎

Lines changed: 13 additions & 0 deletions
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+
deftest_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‎

Lines changed: 30 additions & 15 deletions
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-
classMergeVolumesTest(unittest.TestCase):
57+
classMergePathMappingTest(object):
58+
defconfig_name(self):
59+
return""
60+
5861
deftest_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
deftest_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
deftest_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
deftest_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
deftest_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
deftest_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+
classMergeVolumesTest(unittest.TestCase, MergePathMappingTest):
102+
defconfig_name(self):
103+
return'volumes'
104+
105+
106+
classMergeDevicesTest(unittest.TestCase, MergePathMappingTest):
107+
defconfig_name(self):
108+
return'devices'
109+
96110

111+
classBuildOrImageMergeTest(unittest.TestCase):
97112
deftest_merge_build_or_image_no_override(self):
98113
self.assertEqual(
99114
config.merge_service_dicts({'build': '.'}, {}),

0 commit comments

Comments
 (0)