From 820769e23cc4b822fcf53a3d17b5f544444c9b26 Mon Sep 17 00:00:00 2001 From: Khushiyant Date: Tue, 18 Mar 2025 22:56:15 +0530 Subject: [PATCH 1/2] feat(docker/api/container): add support for subpath in volume_opts TESTED: Yes, added unit tests to verify subpath functionality Signed-off-by: Khushiyant --- docker/types/services.py | 7 +++- tests/integration/api_container_test.py | 50 +++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/docker/types/services.py b/docker/types/services.py index 821115411c..69c0c498ea 100644 --- a/docker/types/services.py +++ b/docker/types/services.py @@ -242,6 +242,7 @@ class Mount(dict): for the ``volume`` type. driver_config (DriverConfig): Volume driver configuration. Only valid for the ``volume`` type. + subpath (str): Path inside a volume to mount instead of the volume root. tmpfs_size (int or string): The size for the tmpfs mount in bytes. tmpfs_mode (int): The permission mode for the tmpfs mount. """ @@ -249,7 +250,7 @@ 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): + tmpfs_mode=None, subpath=None): self['Target'] = target self['Source'] = source if type not in ('bind', 'volume', 'tmpfs', 'npipe'): @@ -267,7 +268,7 @@ def __init__(self, target, source, type='volume', read_only=False, self['BindOptions'] = { 'Propagation': propagation } - if any([labels, driver_config, no_copy, tmpfs_size, tmpfs_mode]): + if any([labels, driver_config, no_copy, tmpfs_size, tmpfs_mode, subpath]): raise errors.InvalidArgument( 'Incompatible options have been provided for the bind ' 'type mount.' @@ -280,6 +281,8 @@ def __init__(self, target, source, type='volume', read_only=False, volume_opts['Labels'] = labels if driver_config: volume_opts['DriverConfig'] = driver_config + if subpath: + volume_opts['Subpath'] = subpath if volume_opts: self['VolumeOptions'] = volume_opts if any([propagation, tmpfs_size, tmpfs_mode]): diff --git a/tests/integration/api_container_test.py b/tests/integration/api_container_test.py index 0215e14c25..21c2f35797 100644 --- a/tests/integration/api_container_test.py +++ b/tests/integration/api_container_test.py @@ -620,6 +620,56 @@ def test_create_with_volume_mount(self): assert mount['Source'] == mount_data['Name'] assert mount_data['RW'] is True + @requires_api_version('1.45') + def test_create_with_subpath_volume_mount(self): + source_volume = helpers.random_name() + self.client.create_volume(name=source_volume) + + setup_container = None + test_container = None + + + # Create a file structure in the volume to test with + setup_container = self.client.create_container( + TEST_IMG, + [ + "sh", + "-c", + 'mkdir -p /vol/subdir && echo "test content" > /vol/subdir/testfile.txt', + ], + host_config=self.client.create_host_config( + binds=[f"{source_volume}:/vol"] + ), + ) + self.client.start(setup_container) + self.client.wait(setup_container) + + # Now test with subpath + mount = docker.types.Mount( + type="volume", + source=source_volume, + target=self.mount_dest, + read_only=True, + subpath="subdir", + ) + + + host_config = self.client.create_host_config(mounts=[mount]) + test_container = self.client.create_container( + TEST_IMG, + ["cat", os.path.join(self.mount_dest, "testfile.txt")], + host_config=host_config, + ) + + self.client.start(test_container) + self.client.wait(test_container) # Wait for container to finish + output = self.client.logs(test_container).decode("utf-8").strip() + + # If the subpath feature is working, we should be able to see the content + # of the file in the subdir + assert output == "test content" + + def check_container_data(self, inspect_data, rw, propagation='rprivate'): assert 'Mounts' in inspect_data filtered = list(filter( From e5c3eb18b6faa5982d967d17ccb18acf44d7a9dc Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Thu, 22 May 2025 01:46:53 +0200 Subject: [PATCH 2/2] integration: adjust tests for omitted "OnBuild" The Docker API may either return an empty "OnBuild" or omit the field altogether if it's not set. Adjust the tests to make either satisfy the test. Signed-off-by: Sebastiaan van Stijn --- tests/integration/api_build_test.py | 2 +- tests/ssh/api_build_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/api_build_test.py b/tests/integration/api_build_test.py index 62e93a7384..0f560159b3 100644 --- a/tests/integration/api_build_test.py +++ b/tests/integration/api_build_test.py @@ -275,7 +275,7 @@ def test_build_container_with_target(self): pass info = self.client.inspect_image('build1') - assert not info['Config']['OnBuild'] + assert 'OnBuild' not in info['Config'] or not info['Config']['OnBuild'] @requires_api_version('1.25') def test_build_with_network_mode(self): diff --git a/tests/ssh/api_build_test.py b/tests/ssh/api_build_test.py index 20476fc74d..f17c75630f 100644 --- a/tests/ssh/api_build_test.py +++ b/tests/ssh/api_build_test.py @@ -266,7 +266,7 @@ def test_build_container_with_target(self): pass info = self.client.inspect_image('build1') - assert not info['Config']['OnBuild'] + assert 'OnBuild' not in info['Config'] or not info['Config']['OnBuild'] @requires_api_version('1.25') def test_build_with_network_mode(self):