mirror of https://github.com/docker/docker-py.git
Add equivalent of bind-recursive option to the Mount type class
With the recursive mount behavior change in Docker 25, it is not possible to make recursive mounts writable with the current API. Add the `recursive` option which is equivalent of bind-recursive in CLI. This also allows for setting the mount to be non-recursive (added earlier in API v1.41). Signed-off-by: Jan Čermák <sairon@sairon.cz>
This commit is contained in:
parent
336e65fc3c
commit
07e35d3f5f
|
@ -235,6 +235,9 @@ class Mount(dict):
|
|||
``default```, ``consistent``, ``cached``, ``delegated``.
|
||||
propagation (string): A propagation mode with the value ``[r]private``,
|
||||
``[r]shared``, or ``[r]slave``. Only valid for the ``bind`` type.
|
||||
recursive (string): Bind mount recursive mode, one of ``enabled``,
|
||||
``disabled``, ``writable``, or ``readonly``. Only valid for the
|
||||
``bind`` type.
|
||||
no_copy (bool): False if the volume should be populated with the data
|
||||
from the target. Default: ``False``. Only valid for the ``volume``
|
||||
type.
|
||||
|
@ -247,9 +250,9 @@ class Mount(dict):
|
|||
"""
|
||||
|
||||
def __init__(self, target, source, type='volume', read_only=False,
|
||||
consistency=None, propagation=None, no_copy=False,
|
||||
labels=None, driver_config=None, tmpfs_size=None,
|
||||
tmpfs_mode=None):
|
||||
consistency=None, propagation=None, recursive=None,
|
||||
no_copy=False, labels=None, driver_config=None,
|
||||
tmpfs_size=None, tmpfs_mode=None):
|
||||
self['Target'] = target
|
||||
self['Source'] = source
|
||||
if type not in ('bind', 'volume', 'tmpfs', 'npipe'):
|
||||
|
@ -267,6 +270,21 @@ class Mount(dict):
|
|||
self['BindOptions'] = {
|
||||
'Propagation': propagation
|
||||
}
|
||||
if recursive is not None:
|
||||
bind_options = self.setdefault('BindOptions', {})
|
||||
if recursive == "enabled":
|
||||
pass # noop - default
|
||||
elif recursive == "disabled":
|
||||
bind_options['NonRecursive'] = True
|
||||
elif recursive == "writable":
|
||||
bind_options['ReadOnlyNonRecursive'] = True
|
||||
elif recursive == "readonly":
|
||||
bind_options['ReadOnlyForceRecursive'] = True
|
||||
else:
|
||||
raise errors.InvalidArgument(
|
||||
'Invalid recursive bind option, must be one of '
|
||||
'"enabled", "disabled", "writable", or "readonly".'
|
||||
)
|
||||
if any([labels, driver_config, no_copy, tmpfs_size, tmpfs_mode]):
|
||||
raise errors.InvalidArgument(
|
||||
'Incompatible options have been provided for the bind '
|
||||
|
@ -282,7 +300,7 @@ class Mount(dict):
|
|||
volume_opts['DriverConfig'] = driver_config
|
||||
if volume_opts:
|
||||
self['VolumeOptions'] = volume_opts
|
||||
if any([propagation, tmpfs_size, tmpfs_mode]):
|
||||
if any([propagation, recursive, tmpfs_size, tmpfs_mode]):
|
||||
raise errors.InvalidArgument(
|
||||
'Incompatible options have been provided for the volume '
|
||||
'type mount.'
|
||||
|
@ -299,7 +317,7 @@ class Mount(dict):
|
|||
tmpfs_opts['SizeBytes'] = parse_bytes(tmpfs_size)
|
||||
if tmpfs_opts:
|
||||
self['TmpfsOptions'] = tmpfs_opts
|
||||
if any([propagation, labels, driver_config, no_copy]):
|
||||
if any([propagation, recursive, labels, driver_config, no_copy]):
|
||||
raise errors.InvalidArgument(
|
||||
'Incompatible options have been provided for the tmpfs '
|
||||
'type mount.'
|
||||
|
|
|
@ -598,6 +598,60 @@ class VolumeBindTest(BaseAPIIntegrationTest):
|
|||
inspect_data = self.client.inspect_container(container)
|
||||
self.check_container_data(inspect_data, False)
|
||||
|
||||
@requires_api_version('1.41')
|
||||
def test_create_with_mounts_recursive_disabled(self):
|
||||
mount = docker.types.Mount(
|
||||
type="bind", source=self.mount_origin, target=self.mount_dest,
|
||||
read_only=True, recursive="disabled"
|
||||
)
|
||||
host_config = self.client.create_host_config(mounts=[mount])
|
||||
container = self.run_container(
|
||||
TEST_IMG, ['ls', self.mount_dest],
|
||||
host_config=host_config
|
||||
)
|
||||
assert container
|
||||
logs = self.client.logs(container).decode('utf-8')
|
||||
assert self.filename in logs
|
||||
inspect_data = self.client.inspect_container(container)
|
||||
self.check_container_data(inspect_data, False,
|
||||
bind_options_field="NonRecursive")
|
||||
|
||||
@requires_api_version('1.44')
|
||||
def test_create_with_mounts_recursive_writable(self):
|
||||
mount = docker.types.Mount(
|
||||
type="bind", source=self.mount_origin, target=self.mount_dest,
|
||||
read_only=True, recursive="writable"
|
||||
)
|
||||
host_config = self.client.create_host_config(mounts=[mount])
|
||||
container = self.run_container(
|
||||
TEST_IMG, ['ls', self.mount_dest],
|
||||
host_config=host_config
|
||||
)
|
||||
assert container
|
||||
logs = self.client.logs(container).decode('utf-8')
|
||||
assert self.filename in logs
|
||||
inspect_data = self.client.inspect_container(container)
|
||||
self.check_container_data(inspect_data, False,
|
||||
bind_options_field="ReadOnlyNonRecursive")
|
||||
|
||||
@requires_api_version('1.44')
|
||||
def test_create_with_mounts_recursive_ro(self):
|
||||
mount = docker.types.Mount(
|
||||
type="bind", source=self.mount_origin, target=self.mount_dest,
|
||||
read_only=True, recursive="readonly"
|
||||
)
|
||||
host_config = self.client.create_host_config(mounts=[mount])
|
||||
container = self.run_container(
|
||||
TEST_IMG, ['ls', self.mount_dest],
|
||||
host_config=host_config
|
||||
)
|
||||
assert container
|
||||
logs = self.client.logs(container).decode('utf-8')
|
||||
assert self.filename in logs
|
||||
inspect_data = self.client.inspect_container(container)
|
||||
self.check_container_data(inspect_data, False,
|
||||
bind_options_field="ReadOnlyForceRecursive")
|
||||
|
||||
@requires_api_version('1.30')
|
||||
def test_create_with_volume_mount(self):
|
||||
mount = docker.types.Mount(
|
||||
|
@ -620,7 +674,8 @@ class VolumeBindTest(BaseAPIIntegrationTest):
|
|||
assert mount['Source'] == mount_data['Name']
|
||||
assert mount_data['RW'] is True
|
||||
|
||||
def check_container_data(self, inspect_data, rw, propagation='rprivate'):
|
||||
def check_container_data(self, inspect_data, rw, propagation='rprivate',
|
||||
bind_options_field=None):
|
||||
assert 'Mounts' in inspect_data
|
||||
filtered = list(filter(
|
||||
lambda x: x['Destination'] == self.mount_dest,
|
||||
|
@ -631,6 +686,18 @@ class VolumeBindTest(BaseAPIIntegrationTest):
|
|||
assert mount_data['Source'] == self.mount_origin
|
||||
assert mount_data['RW'] == rw
|
||||
assert mount_data['Propagation'] == propagation
|
||||
if bind_options_field:
|
||||
assert 'Mounts' in inspect_data['HostConfig']
|
||||
mounts = [
|
||||
x for x in inspect_data['HostConfig']['Mounts']
|
||||
if x['Target'] == self.mount_dest
|
||||
]
|
||||
assert len(mounts) == 1
|
||||
mount = mounts[0]
|
||||
assert 'BindOptions' in mount
|
||||
bind_options = mount['BindOptions']
|
||||
assert bind_options_field in bind_options
|
||||
assert bind_options[bind_options_field] is True
|
||||
|
||||
def run_with_volume(self, ro, *args, **kwargs):
|
||||
return self.run_container(
|
||||
|
|
Loading…
Reference in New Issue