Skip to content

Commit

Permalink
Add equivalent of bind-recursive option to the Mount type class
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
sairon committed Apr 4, 2024
1 parent 336e65f commit ee5a45d
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 6 deletions.
28 changes: 23 additions & 5 deletions docker/types/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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'):
Expand All @@ -267,6 +270,21 @@ def __init__(self, target, source, type='volume', read_only=False,
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 '
Expand All @@ -282,7 +300,7 @@ def __init__(self, target, source, type='volume', read_only=False,
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.'
Expand All @@ -299,7 +317,7 @@ def __init__(self, target, source, type='volume', read_only=False,
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.'
Expand Down
69 changes: 68 additions & 1 deletion tests/integration/api_container_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,60 @@ def test_create_with_mounts_ro(self):
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(
Expand All @@ -620,7 +674,8 @@ def test_create_with_volume_mount(self):
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,
Expand All @@ -631,6 +686,18 @@ def check_container_data(self, inspect_data, rw, propagation='rprivate'):
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(
Expand Down

0 comments on commit ee5a45d

Please sign in to comment.