diff --git a/docker/api/container.py b/docker/api/container.py index ec28fd581..5a267d13f 100644 --- a/docker/api/container.py +++ b/docker/api/container.py @@ -319,6 +319,11 @@ def create_container(self, image, command=None, hostname=None, user=None, '/var/www': { 'bind': '/mnt/vol1', 'mode': 'ro', + }, + '/autofs/user1': { + 'bind': '/mnt/vol3', + 'mode': 'rw', + 'propagation': 'shared' } }) ) @@ -329,10 +334,11 @@ def create_container(self, image, command=None, hostname=None, user=None, .. code-block:: python container_id = client.api.create_container( - 'busybox', 'ls', volumes=['/mnt/vol1', '/mnt/vol2'], + 'busybox', 'ls', volumes=['/mnt/vol1', '/mnt/vol2', '/mnt/vol3'], host_config=client.api.create_host_config(binds=[ '/home/user1/:/mnt/vol2', '/var/www:/mnt/vol1:ro', + '/autofs/user1:/mnt/vol3:rw,shared', ]) ) diff --git a/docker/utils/utils.py b/docker/utils/utils.py index 4affeb339..0f28afb11 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -17,7 +17,6 @@ from urllib.parse import urlparse, urlunparse - URLComponents = collections.namedtuple( 'URLComponents', 'scheme netloc url params query fragment', @@ -141,6 +140,22 @@ def convert_volume_binds(binds): else: mode = 'rw' + # NOTE: this is only relevant for Linux hosts + # (doesn't apply in Docker Desktop) + propagation_modes = [ + 'rshared', + 'shared', + 'rslave', + 'slave', + 'rprivate', + 'private', + ] + if 'propagation' in v and v['propagation'] in propagation_modes: + if mode: + mode = ','.join([mode, v['propagation']]) + else: + mode = v['propagation'] + result.append( f'{k}:{bind}:{mode}' ) diff --git a/tests/helpers.py b/tests/helpers.py index e0785774b..748ee70a7 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -46,6 +46,19 @@ def untar_file(tardata, filename): return result +def skip_if_desktop(): + def fn(f): + @functools.wraps(f) + def wrapped(self, *args, **kwargs): + info = self.client.info() + if info['Name'] == 'docker-desktop': + pytest.skip('Test does not support Docker Desktop') + return f(self, *args, **kwargs) + + return wrapped + + return fn + def requires_api_version(version): test_version = os.environ.get( 'DOCKER_TEST_API_VERSION', docker.constants.DEFAULT_DOCKER_API_VERSION diff --git a/tests/integration/api_container_test.py b/tests/integration/api_container_test.py index 590c4fa0c..aa27fbfd7 100644 --- a/tests/integration/api_container_test.py +++ b/tests/integration/api_container_test.py @@ -12,7 +12,7 @@ from .. import helpers from ..helpers import assert_cat_socket_detached_with_keys from ..helpers import ctrl_with -from ..helpers import requires_api_version +from ..helpers import requires_api_version, skip_if_desktop from .base import BaseAPIIntegrationTest from .base import TEST_IMG from docker.constants import IS_WINDOWS_PLATFORM @@ -542,6 +542,27 @@ def test_create_with_binds_ro(self): inspect_data = self.client.inspect_container(container) self.check_container_data(inspect_data, False) + @skip_if_desktop() + def test_create_with_binds_rw_rshared(self): + container = self.run_with_volume_propagation( + False, + 'rshared', + TEST_IMG, + ['touch', os.path.join(self.mount_dest, self.filename)], + ) + inspect_data = self.client.inspect_container(container) + self.check_container_data(inspect_data, True, 'rshared') + container = self.run_with_volume_propagation( + True, + 'rshared', + TEST_IMG, + ['ls', self.mount_dest], + ) + 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, 'rshared') + @requires_api_version('1.30') def test_create_with_mounts(self): mount = docker.types.Mount( @@ -597,7 +618,7 @@ 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): + def check_container_data(self, inspect_data, rw, propagation='rprivate'): assert 'Mounts' in inspect_data filtered = list(filter( lambda x: x['Destination'] == self.mount_dest, @@ -607,6 +628,7 @@ def check_container_data(self, inspect_data, rw): mount_data = filtered[0] assert mount_data['Source'] == self.mount_origin assert mount_data['RW'] == rw + assert mount_data['Propagation'] == propagation def run_with_volume(self, ro, *args, **kwargs): return self.run_container( @@ -624,6 +646,23 @@ def run_with_volume(self, ro, *args, **kwargs): **kwargs ) + def run_with_volume_propagation(self, ro, propagation, *args, **kwargs): + return self.run_container( + *args, + volumes={self.mount_dest: {}}, + host_config=self.client.create_host_config( + binds={ + self.mount_origin: { + 'bind': self.mount_dest, + 'ro': ro, + 'propagation': propagation + }, + }, + network_mode='none' + ), + **kwargs + ) + class ArchiveTest(BaseAPIIntegrationTest): def test_get_file_archive_from_container(self):