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:
Jan Čermák 2024-04-04 11:32:44 +02:00
parent 336e65fc3c
commit 07e35d3f5f
No known key found for this signature in database
GPG Key ID: A78C897AA3AF012B
2 changed files with 91 additions and 6 deletions

View File

@ -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.'

View File

@ -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(