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``. ``default```, ``consistent``, ``cached``, ``delegated``.
propagation (string): A propagation mode with the value ``[r]private``, propagation (string): A propagation mode with the value ``[r]private``,
``[r]shared``, or ``[r]slave``. Only valid for the ``bind`` type. ``[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 no_copy (bool): False if the volume should be populated with the data
from the target. Default: ``False``. Only valid for the ``volume`` from the target. Default: ``False``. Only valid for the ``volume``
type. type.
@ -247,9 +250,9 @@ class Mount(dict):
""" """
def __init__(self, target, source, type='volume', read_only=False, def __init__(self, target, source, type='volume', read_only=False,
consistency=None, propagation=None, no_copy=False, consistency=None, propagation=None, recursive=None,
labels=None, driver_config=None, tmpfs_size=None, no_copy=False, labels=None, driver_config=None,
tmpfs_mode=None): tmpfs_size=None, tmpfs_mode=None):
self['Target'] = target self['Target'] = target
self['Source'] = source self['Source'] = source
if type not in ('bind', 'volume', 'tmpfs', 'npipe'): if type not in ('bind', 'volume', 'tmpfs', 'npipe'):
@ -267,6 +270,21 @@ class Mount(dict):
self['BindOptions'] = { self['BindOptions'] = {
'Propagation': propagation '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]): if any([labels, driver_config, no_copy, tmpfs_size, tmpfs_mode]):
raise errors.InvalidArgument( raise errors.InvalidArgument(
'Incompatible options have been provided for the bind ' 'Incompatible options have been provided for the bind '
@ -282,7 +300,7 @@ class Mount(dict):
volume_opts['DriverConfig'] = driver_config volume_opts['DriverConfig'] = driver_config
if volume_opts: if volume_opts:
self['VolumeOptions'] = volume_opts self['VolumeOptions'] = volume_opts
if any([propagation, tmpfs_size, tmpfs_mode]): if any([propagation, recursive, tmpfs_size, tmpfs_mode]):
raise errors.InvalidArgument( raise errors.InvalidArgument(
'Incompatible options have been provided for the volume ' 'Incompatible options have been provided for the volume '
'type mount.' 'type mount.'
@ -299,7 +317,7 @@ class Mount(dict):
tmpfs_opts['SizeBytes'] = parse_bytes(tmpfs_size) tmpfs_opts['SizeBytes'] = parse_bytes(tmpfs_size)
if tmpfs_opts: if tmpfs_opts:
self['TmpfsOptions'] = 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( raise errors.InvalidArgument(
'Incompatible options have been provided for the tmpfs ' 'Incompatible options have been provided for the tmpfs '
'type mount.' 'type mount.'

View File

@ -598,6 +598,60 @@ class VolumeBindTest(BaseAPIIntegrationTest):
inspect_data = self.client.inspect_container(container) inspect_data = self.client.inspect_container(container)
self.check_container_data(inspect_data, False) 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') @requires_api_version('1.30')
def test_create_with_volume_mount(self): def test_create_with_volume_mount(self):
mount = docker.types.Mount( mount = docker.types.Mount(
@ -620,7 +674,8 @@ class VolumeBindTest(BaseAPIIntegrationTest):
assert mount['Source'] == mount_data['Name'] assert mount['Source'] == mount_data['Name']
assert mount_data['RW'] is True 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 assert 'Mounts' in inspect_data
filtered = list(filter( filtered = list(filter(
lambda x: x['Destination'] == self.mount_dest, lambda x: x['Destination'] == self.mount_dest,
@ -631,6 +686,18 @@ class VolumeBindTest(BaseAPIIntegrationTest):
assert mount_data['Source'] == self.mount_origin assert mount_data['Source'] == self.mount_origin
assert mount_data['RW'] == rw assert mount_data['RW'] == rw
assert mount_data['Propagation'] == propagation 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): def run_with_volume(self, ro, *args, **kwargs):
return self.run_container( return self.run_container(