Skip to content

Commit ee5a45d

Browse files
committed
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 <[email protected]>
1 parent 336e65f commit ee5a45d

File tree

2 files changed

+91
-6
lines changed

2 files changed

+91
-6
lines changed

docker/types/services.py

+23-5
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,9 @@ class Mount(dict):
235235
``default```, ``consistent``, ``cached``, ``delegated``.
236236
propagation (string): A propagation mode with the value ``[r]private``,
237237
``[r]shared``, or ``[r]slave``. Only valid for the ``bind`` type.
238+
recursive (string): Bind mount recursive mode, one of ``enabled``,
239+
``disabled``, ``writable``, or ``readonly``. Only valid for the
240+
``bind`` type.
238241
no_copy (bool): False if the volume should be populated with the data
239242
from the target. Default: ``False``. Only valid for the ``volume``
240243
type.
@@ -247,9 +250,9 @@ class Mount(dict):
247250
"""
248251

249252
def __init__(self, target, source, type='volume', read_only=False,
250-
consistency=None, propagation=None, no_copy=False,
251-
labels=None, driver_config=None, tmpfs_size=None,
252-
tmpfs_mode=None):
253+
consistency=None, propagation=None, recursive=None,
254+
no_copy=False, labels=None, driver_config=None,
255+
tmpfs_size=None, tmpfs_mode=None):
253256
self['Target'] = target
254257
self['Source'] = source
255258
if type not in ('bind', 'volume', 'tmpfs', 'npipe'):
@@ -267,6 +270,21 @@ def __init__(self, target, source, type='volume', read_only=False,
267270
self['BindOptions'] = {
268271
'Propagation': propagation
269272
}
273+
if recursive is not None:
274+
bind_options = self.setdefault('BindOptions', {})
275+
if recursive == "enabled":
276+
pass # noop - default
277+
elif recursive == "disabled":
278+
bind_options['NonRecursive'] = True
279+
elif recursive == "writable":
280+
bind_options['ReadOnlyNonRecursive'] = True
281+
elif recursive == "readonly":
282+
bind_options['ReadOnlyForceRecursive'] = True
283+
else:
284+
raise errors.InvalidArgument(
285+
'Invalid recursive bind option, must be one of '
286+
'"enabled", "disabled", "writable", or "readonly".'
287+
)
270288
if any([labels, driver_config, no_copy, tmpfs_size, tmpfs_mode]):
271289
raise errors.InvalidArgument(
272290
'Incompatible options have been provided for the bind '
@@ -282,7 +300,7 @@ def __init__(self, target, source, type='volume', read_only=False,
282300
volume_opts['DriverConfig'] = driver_config
283301
if volume_opts:
284302
self['VolumeOptions'] = volume_opts
285-
if any([propagation, tmpfs_size, tmpfs_mode]):
303+
if any([propagation, recursive, tmpfs_size, tmpfs_mode]):
286304
raise errors.InvalidArgument(
287305
'Incompatible options have been provided for the volume '
288306
'type mount.'
@@ -299,7 +317,7 @@ def __init__(self, target, source, type='volume', read_only=False,
299317
tmpfs_opts['SizeBytes'] = parse_bytes(tmpfs_size)
300318
if tmpfs_opts:
301319
self['TmpfsOptions'] = tmpfs_opts
302-
if any([propagation, labels, driver_config, no_copy]):
320+
if any([propagation, recursive, labels, driver_config, no_copy]):
303321
raise errors.InvalidArgument(
304322
'Incompatible options have been provided for the tmpfs '
305323
'type mount.'

tests/integration/api_container_test.py

+68-1
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,60 @@ def test_create_with_mounts_ro(self):
598598
inspect_data = self.client.inspect_container(container)
599599
self.check_container_data(inspect_data, False)
600600

601+
@requires_api_version('1.41')
602+
def test_create_with_mounts_recursive_disabled(self):
603+
mount = docker.types.Mount(
604+
type="bind", source=self.mount_origin, target=self.mount_dest,
605+
read_only=True, recursive="disabled"
606+
)
607+
host_config = self.client.create_host_config(mounts=[mount])
608+
container = self.run_container(
609+
TEST_IMG, ['ls', self.mount_dest],
610+
host_config=host_config
611+
)
612+
assert container
613+
logs = self.client.logs(container).decode('utf-8')
614+
assert self.filename in logs
615+
inspect_data = self.client.inspect_container(container)
616+
self.check_container_data(inspect_data, False,
617+
bind_options_field="NonRecursive")
618+
619+
@requires_api_version('1.44')
620+
def test_create_with_mounts_recursive_writable(self):
621+
mount = docker.types.Mount(
622+
type="bind", source=self.mount_origin, target=self.mount_dest,
623+
read_only=True, recursive="writable"
624+
)
625+
host_config = self.client.create_host_config(mounts=[mount])
626+
container = self.run_container(
627+
TEST_IMG, ['ls', self.mount_dest],
628+
host_config=host_config
629+
)
630+
assert container
631+
logs = self.client.logs(container).decode('utf-8')
632+
assert self.filename in logs
633+
inspect_data = self.client.inspect_container(container)
634+
self.check_container_data(inspect_data, False,
635+
bind_options_field="ReadOnlyNonRecursive")
636+
637+
@requires_api_version('1.44')
638+
def test_create_with_mounts_recursive_ro(self):
639+
mount = docker.types.Mount(
640+
type="bind", source=self.mount_origin, target=self.mount_dest,
641+
read_only=True, recursive="readonly"
642+
)
643+
host_config = self.client.create_host_config(mounts=[mount])
644+
container = self.run_container(
645+
TEST_IMG, ['ls', self.mount_dest],
646+
host_config=host_config
647+
)
648+
assert container
649+
logs = self.client.logs(container).decode('utf-8')
650+
assert self.filename in logs
651+
inspect_data = self.client.inspect_container(container)
652+
self.check_container_data(inspect_data, False,
653+
bind_options_field="ReadOnlyForceRecursive")
654+
601655
@requires_api_version('1.30')
602656
def test_create_with_volume_mount(self):
603657
mount = docker.types.Mount(
@@ -620,7 +674,8 @@ def test_create_with_volume_mount(self):
620674
assert mount['Source'] == mount_data['Name']
621675
assert mount_data['RW'] is True
622676

623-
def check_container_data(self, inspect_data, rw, propagation='rprivate'):
677+
def check_container_data(self, inspect_data, rw, propagation='rprivate',
678+
bind_options_field=None):
624679
assert 'Mounts' in inspect_data
625680
filtered = list(filter(
626681
lambda x: x['Destination'] == self.mount_dest,
@@ -631,6 +686,18 @@ def check_container_data(self, inspect_data, rw, propagation='rprivate'):
631686
assert mount_data['Source'] == self.mount_origin
632687
assert mount_data['RW'] == rw
633688
assert mount_data['Propagation'] == propagation
689+
if bind_options_field:
690+
assert 'Mounts' in inspect_data['HostConfig']
691+
mounts = [
692+
x for x in inspect_data['HostConfig']['Mounts']
693+
if x['Target'] == self.mount_dest
694+
]
695+
assert len(mounts) == 1
696+
mount = mounts[0]
697+
assert 'BindOptions' in mount
698+
bind_options = mount['BindOptions']
699+
assert bind_options_field in bind_options
700+
assert bind_options[bind_options_field] is True
634701

635702
def run_with_volume(self, ro, *args, **kwargs):
636703
return self.run_container(

0 commit comments

Comments
 (0)