mirror of https://github.com/docker/compose.git
Added devices configuration option
Signed-off-by: Dan Elbert <dan.elbert@gmail.com>
This commit is contained in:
parent
1748b0f81a
commit
df87bd91c8
|
@ -10,6 +10,7 @@ DOCKER_CONFIG_KEYS = [
|
|||
'cpuset',
|
||||
'command',
|
||||
'detach',
|
||||
'devices',
|
||||
'dns',
|
||||
'dns_search',
|
||||
'domainname',
|
||||
|
@ -50,6 +51,7 @@ DOCKER_CONFIG_HINTS = {
|
|||
'add_host': 'extra_hosts',
|
||||
'hosts': 'extra_hosts',
|
||||
'extra_host': 'extra_hosts',
|
||||
'device': 'devices',
|
||||
'link': 'links',
|
||||
'port': 'ports',
|
||||
'privilege': 'privileged',
|
||||
|
@ -200,11 +202,14 @@ def merge_service_dicts(base, override):
|
|||
override.get('environment'),
|
||||
)
|
||||
|
||||
if 'volumes' in base or 'volumes' in override:
|
||||
d['volumes'] = merge_volumes(
|
||||
base.get('volumes'),
|
||||
override.get('volumes'),
|
||||
)
|
||||
path_mapping_keys = ['volumes', 'devices']
|
||||
|
||||
for key in path_mapping_keys:
|
||||
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:
|
||||
d['labels'] = merge_labels(
|
||||
|
@ -230,7 +235,7 @@ def merge_service_dicts(base, override):
|
|||
if key in base or key in override:
|
||||
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):
|
||||
if k in override:
|
||||
|
@ -346,7 +351,7 @@ def resolve_host_paths(volumes, working_dir=None):
|
|||
|
||||
|
||||
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:
|
||||
host_path = os.path.expanduser(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)
|
||||
|
||||
|
||||
def merge_volumes(base, override):
|
||||
d = dict_from_volumes(base)
|
||||
d.update(dict_from_volumes(override))
|
||||
return volumes_from_dict(d)
|
||||
def merge_path_mappings(base, override):
|
||||
d = dict_from_path_mappings(base)
|
||||
d.update(dict_from_path_mappings(override))
|
||||
return path_mappings_from_dict(d)
|
||||
|
||||
|
||||
def dict_from_volumes(volumes):
|
||||
if volumes:
|
||||
return dict(split_volume(v) for v in volumes)
|
||||
def dict_from_path_mappings(path_mappings):
|
||||
if path_mappings:
|
||||
return dict(split_path_mapping(v) for v in path_mappings)
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
||||
def volumes_from_dict(d):
|
||||
return [join_volume(v) for v in d.items()]
|
||||
def path_mappings_from_dict(d):
|
||||
return [join_path_mapping(v) for v in d.items()]
|
||||
|
||||
|
||||
def split_volume(string):
|
||||
def split_path_mapping(string):
|
||||
if ':' in string:
|
||||
(host, container) = string.split(':', 1)
|
||||
return (container, host)
|
||||
|
@ -393,7 +398,7 @@ def split_volume(string):
|
|||
return (string, None)
|
||||
|
||||
|
||||
def join_volume(pair):
|
||||
def join_path_mapping(pair):
|
||||
(container, host) = pair
|
||||
if host is None:
|
||||
return container
|
||||
|
|
|
@ -20,6 +20,7 @@ log = logging.getLogger(__name__)
|
|||
DOCKER_START_KEYS = [
|
||||
'cap_add',
|
||||
'cap_drop',
|
||||
'devices',
|
||||
'dns',
|
||||
'dns_search',
|
||||
'env_file',
|
||||
|
@ -441,6 +442,8 @@ class Service(object):
|
|||
extra_hosts = build_extra_hosts(options.get('extra_hosts', None))
|
||||
read_only = options.get('read_only', None)
|
||||
|
||||
devices = options.get('devices', None)
|
||||
|
||||
return create_host_config(
|
||||
links=self._get_links(link_to_self=one_off),
|
||||
port_bindings=port_bindings,
|
||||
|
@ -448,6 +451,7 @@ class Service(object):
|
|||
volumes_from=options.get('volumes_from'),
|
||||
privileged=privileged,
|
||||
network_mode=self._get_net(),
|
||||
devices=devices,
|
||||
dns=dns,
|
||||
dns_search=dns_search,
|
||||
restart_policy=restart,
|
||||
|
|
|
@ -342,8 +342,8 @@ environment:
|
|||
- BAZ=local
|
||||
```
|
||||
|
||||
Finally, for `volumes`, Compose "merges" entries together with locally-defined
|
||||
bindings taking precedence:
|
||||
Finally, for `volumes` and `devices`, Compose "merges" entries together with
|
||||
locally-defined bindings taking precedence:
|
||||
|
||||
```yaml
|
||||
# original service
|
||||
|
@ -361,4 +361,4 @@ volumes:
|
|||
- /original-dir/foo:/foo
|
||||
- /local-dir/bar:/bar
|
||||
- /local-dir/baz/:baz
|
||||
```
|
||||
```
|
||||
|
|
14
docs/yml.md
14
docs/yml.md
|
@ -29,8 +29,8 @@ image: a4bc65fd
|
|||
|
||||
### build
|
||||
|
||||
Path to a directory containing a Dockerfile. When the value supplied is a
|
||||
relative path, it is interpreted as relative to the location of the yml file
|
||||
Path to a directory containing a Dockerfile. When the value supplied is a
|
||||
relative path, it is interpreted as relative to the location of the yml file
|
||||
itself. This directory is also the build context that is sent to the Docker daemon.
|
||||
|
||||
Compose will build and tag it with a generated name, and use that image thereafter.
|
||||
|
@ -342,6 +342,16 @@ dns_search:
|
|||
- 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
|
||||
|
||||
Each of these is a single value, analogous to its
|
||||
|
|
|
@ -669,3 +669,16 @@ class ServiceTest(DockerClientTestCase):
|
|||
|
||||
self.assertEqual('none', log_config['Type'])
|
||||
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'])
|
||||
|
||||
|
||||
class MergeVolumesTest(unittest.TestCase):
|
||||
class MergePathMappingTest(object):
|
||||
def config_name(self):
|
||||
return ""
|
||||
|
||||
def test_empty(self):
|
||||
service_dict = config.merge_service_dicts({}, {})
|
||||
self.assertNotIn('volumes', service_dict)
|
||||
self.assertNotIn(self.config_name(), service_dict)
|
||||
|
||||
def test_no_override(self):
|
||||
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):
|
||||
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):
|
||||
service_dict = config.merge_service_dicts(
|
||||
{'volumes': ['/foo:/code', '/data']},
|
||||
{'volumes': ['/bar:/code']},
|
||||
{self.config_name(): ['/foo:/code', '/data']},
|
||||
{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):
|
||||
service_dict = config.merge_service_dicts(
|
||||
{'volumes': ['/foo:/code', '/data']},
|
||||
{'volumes': ['/bar:/code', '/quux:/data']},
|
||||
{self.config_name(): ['/foo:/code', '/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):
|
||||
service_dict = config.merge_service_dicts(
|
||||
{'volumes': ['/foo:/code', '/quux:/data']},
|
||||
{'volumes': ['/bar:/code', '/data']},
|
||||
{self.config_name(): ['/foo:/code', '/quux:/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):
|
||||
self.assertEqual(
|
||||
config.merge_service_dicts({'build': '.'}, {}),
|
||||
|
|
Loading…
Reference in New Issue