From 37ed118b018a1ad8699181835d5cfcd2eb0e2325 Mon Sep 17 00:00:00 2001 From: Atte Lautanala Date: Tue, 18 Nov 2025 17:08:46 +0200 Subject: [PATCH] Implement 'image' volume type for services This adds support for volume.type=image in services, as specified in [Compose spec](https://github.com/compose-spec/compose-spec/blob/76d4a3d08f9d4eb251092746394a64327031a6c6/05-services.md#long-syntax-5). Fixes https://github.com/containers/podman-compose/issues/1202 Signed-off-by: Atte Lautanala --- newsfragments/mount-image-as-volume.feature | 1 + podman_compose.py | 9 ++++- tests/unit/test_container_to_args.py | 41 +++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 newsfragments/mount-image-as-volume.feature diff --git a/newsfragments/mount-image-as-volume.feature b/newsfragments/mount-image-as-volume.feature new file mode 100644 index 00000000..76c777f5 --- /dev/null +++ b/newsfragments/mount-image-as-volume.feature @@ -0,0 +1 @@ +Support volume.type=image in services, for mounting files from container images. diff --git a/podman_compose.py b/podman_compose.py index bf8119b3..794f609a 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -480,6 +480,11 @@ def mount_desc_to_mount_args(mount_desc: dict[str, Any]) -> str: selinux = bind_opts.get("selinux") if selinux is not None: opts.append(selinux) + if mount_type == "image": + image_opts = mount_desc.get("image", {}) + subpath = image_opts.get("subpath") + if subpath is not None: + opts.append(f"subpath={subpath}") opts_str = ",".join(opts) if mount_type == "bind": return f"type=bind,source={source},destination={target},{opts_str}".rstrip(",") @@ -487,6 +492,8 @@ def mount_desc_to_mount_args(mount_desc: dict[str, Any]) -> str: return f"type=volume,source={source},destination={target},{opts_str}".rstrip(",") if mount_type == "tmpfs": return f"type=tmpfs,destination={target},{opts_str}".rstrip(",") + if mount_type == "image": + return f"type=image,source={source},destination={target},{opts_str}".rstrip(",") raise ValueError("unknown mount type:" + mount_type) @@ -574,7 +581,7 @@ async def get_mount_args( srv_name = cnt["_service"] mount_type = volume["type"] await assert_volume(compose, volume) - if compose.prefer_volume_over_mount: + if compose.prefer_volume_over_mount and mount_type != "image": if mount_type == "tmpfs": # TODO: --tmpfs /tmp:rw,size=787448k,mode=1777 args = volume["target"] diff --git a/tests/unit/test_container_to_args.py b/tests/unit/test_container_to_args.py index 3d035ae4..60672e6c 100644 --- a/tests/unit/test_container_to_args.py +++ b/tests/unit/test_container_to_args.py @@ -680,6 +680,47 @@ async def test_volumes_bind_mount_source( ], ) + @parameterized.expand([ + ( + "without_subpath", + {}, + "type=image,source=example:latest,destination=/mnt/example", + ), + ( + "with_subpath", + {"image": {"subpath": "path/to/image/folder"}}, + "type=image,source=example:latest,destination=/mnt/example,subpath=path/to/image/folder", + ), + ]) + async def test_volumes_image_mount( + self, test_name: str, image_opts: dict, expected_mount_arg: str + ) -> None: + c = create_compose_mock() + cnt = get_minimal_container() + cnt["_service"] = cnt["service_name"] + + cnt["volumes"] = [ + { + "type": "image", + "source": "example:latest", + "target": "/mnt/example", + **image_opts, + }, + ] + + args = await container_to_args(c, cnt) + self.assertEqual( + args, + [ + "--name=project_name_service_name1", + "-d", + "--mount", + expected_mount_arg, + "--network=bridge:alias=service_name", + "busybox", + ], + ) + @parameterized.expand([ ( "create_host_path_set_to_true",