Skip to content

Commit f618ff3

Browse files
authored
Merge pull request #1279 from mokibit/add-volume-bind-create-host-path-option
Add volume bind `create_host_path` option
2 parents af7346b + 4f9b419 commit f618ff3

File tree

9 files changed

+256
-0
lines changed

9 files changed

+256
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Implemented volumes bind `create_host_path` option.

podman_compose.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,12 @@ async def assert_volume(compose: PodmanCompose, mount_dict: dict[str, Any]) -> N
405405
mount_src = mount_dict["source"]
406406
mount_src = os.path.realpath(os.path.join(basedir, os.path.expanduser(mount_src)))
407407
if not os.path.exists(mount_src):
408+
bind_opts = mount_dict.get("bind", {})
409+
if "create_host_path" in bind_opts and not bind_opts["create_host_path"]:
410+
raise ValueError(
411+
"invalid mount config for type 'bind': bind source path does not exist: "
412+
f"{mount_src}"
413+
)
408414
try:
409415
os.makedirs(mount_src, exist_ok=True)
410416
except OSError:

tests/integration/vol/long_syntax/__init__.py

Whitespace-only changes.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
version: "3"
2+
services:
3+
create_host_path_default_true: # default is to always create
4+
image: nopush/podman-compose-test
5+
command: ["/bin/busybox", "mkdir", "cont/test_dir/new_dir"]
6+
volumes:
7+
- type: bind
8+
source: ./test_dir
9+
target: /cont/test_dir
10+
create_host_path_true:
11+
image: nopush/podman-compose-test
12+
command: ["/bin/busybox", "mkdir", "cont/test_dir/new_dir"]
13+
volumes:
14+
- type: bind
15+
source: ./test_dir
16+
target: /cont/test_dir
17+
bind:
18+
create_host_path: true
19+
create_host_path_false:
20+
image: nopush/podman-compose-test
21+
command: ["/bin/busybox", "mkdir", "cont/test_dir/new_dir"]
22+
volumes:
23+
- type: bind
24+
source: ./test_dir
25+
target: /cont/test_dir
26+
bind:
27+
create_host_path: false
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# SPDX-License-Identifier: GPL-2.0
2+
3+
import os
4+
import shutil
5+
import unittest
6+
7+
from parameterized import parameterized
8+
9+
from tests.integration.test_utils import RunSubprocessMixin
10+
from tests.integration.test_utils import podman_compose_path
11+
from tests.integration.test_utils import test_path
12+
13+
14+
def compose_yaml_path(test_ref_folder: str) -> str:
15+
return os.path.join(test_path(), "vol", test_ref_folder, "docker-compose.yml")
16+
17+
18+
class TestComposeVolLongSyntax(unittest.TestCase, RunSubprocessMixin):
19+
@parameterized.expand([
20+
(True, "create_host_path_default_true"),
21+
(True, "create_host_path_true"),
22+
(True, "create_host_path_false"),
23+
(False, "create_host_path_default_true"),
24+
(False, "create_host_path_true"),
25+
])
26+
def test_source_host_dir(self, source_dir_exists: bool, service_name: str) -> None:
27+
project_dir = os.path.join(test_path(), "vol", "long_syntax")
28+
if source_dir_exists:
29+
# create host source directory for volume
30+
os.mkdir(os.path.join(project_dir, "test_dir"))
31+
else:
32+
# make sure there is no such directory
33+
self.assertFalse(os.path.isdir(os.path.join(project_dir, "test_dir")))
34+
35+
try:
36+
self.run_subprocess_assert_returncode(
37+
[
38+
podman_compose_path(),
39+
"-f",
40+
compose_yaml_path("long_syntax"),
41+
"up",
42+
"-d",
43+
f"{service_name}",
44+
],
45+
0,
46+
)
47+
# command of the service creates a new directory on mounted directory 'test_dir' in
48+
# the container. Check if host directory now has the same directory. It represents a
49+
# successful mount.
50+
# If source host directory does not exist, it is created
51+
self.assertTrue(os.path.isdir(os.path.join(project_dir, "test_dir/new_dir")))
52+
53+
finally:
54+
self.run_subprocess_assert_returncode([
55+
podman_compose_path(),
56+
"-f",
57+
compose_yaml_path("long_syntax"),
58+
"down",
59+
"-t",
60+
"0",
61+
])
62+
shutil.rmtree(os.path.join(project_dir, "test_dir"))
63+
64+
def test_no_host_source_dir_create_host_path_false(self) -> None:
65+
try:
66+
_, error = self.run_subprocess_assert_returncode(
67+
[
68+
podman_compose_path(),
69+
"-f",
70+
compose_yaml_path("long_syntax"),
71+
"up",
72+
"-d",
73+
"create_host_path_false",
74+
],
75+
1,
76+
)
77+
self.assertIn(
78+
b"invalid mount config for type 'bind': bind source path does not exist:", error
79+
)
80+
finally:
81+
self.run_subprocess([
82+
podman_compose_path(),
83+
"-f",
84+
compose_yaml_path("long_syntax"),
85+
"down",
86+
"-t",
87+
"0",
88+
])

tests/integration/vol/short_syntax/__init__.py

Whitespace-only changes.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
version: "3"
2+
services:
3+
test1:
4+
image: nopush/podman-compose-test
5+
#["/bin/busybox", "sh", "-c", "env | grep ZZVAR3"]
6+
command: ["/bin/busybox", "mkdir", "cont/test_dir/new_dir"]
7+
volumes:
8+
- ./test_dir:/cont/test_dir
9+
test2:
10+
image: nopush/podman-compose-test
11+
command: ["/bin/busybox", "mkdir", "cont/test_dir/new_dir"]
12+
volumes:
13+
- ./test_dir:/cont/test_dir
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# SPDX-License-Identifier: GPL-2.0
2+
3+
import os
4+
import shutil
5+
import unittest
6+
7+
from parameterized import parameterized
8+
9+
from tests.integration.test_utils import RunSubprocessMixin
10+
from tests.integration.test_utils import podman_compose_path
11+
from tests.integration.test_utils import test_path
12+
13+
14+
def compose_yaml_path(test_ref_folder: str) -> str:
15+
return os.path.join(test_path(), "vol", test_ref_folder, "docker-compose.yml")
16+
17+
18+
class TestComposeVolShortSyntax(unittest.TestCase, RunSubprocessMixin):
19+
@parameterized.expand([
20+
(True, "test1"),
21+
(False, "test2"),
22+
])
23+
def test_source_host_dir(self, source_host_dir_exists: bool, service_name: str) -> None:
24+
project_dir = os.path.join(test_path(), "vol", "short_syntax")
25+
# create host source directory for volume
26+
if source_host_dir_exists:
27+
os.mkdir(os.path.join(project_dir, "test_dir"))
28+
try:
29+
self.run_subprocess_assert_returncode(
30+
[
31+
podman_compose_path(),
32+
"-f",
33+
compose_yaml_path("short_syntax"),
34+
"up",
35+
"-d",
36+
f"{service_name}",
37+
],
38+
0,
39+
)
40+
# command of the service creates a new directory on mounted directory 'test_dir' in
41+
# container. Check if host directory now has the same directory. It represents a
42+
# successful mount.
43+
# On service test2 source host directory is created as it did not exist
44+
self.assertTrue(os.path.isdir(os.path.join(project_dir, "test_dir/new_dir")))
45+
46+
finally:
47+
self.run_subprocess([
48+
podman_compose_path(),
49+
"-f",
50+
compose_yaml_path("short_syntax"),
51+
"down",
52+
"-t",
53+
"0",
54+
])
55+
shutil.rmtree(os.path.join(project_dir, "test_dir"))

tests/unit/test_container_to_args.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# SPDX-License-Identifier: GPL-2.0
22

33
import os
4+
import shutil
45
import unittest
56
from typing import Any
67
from unittest import mock
@@ -679,6 +680,71 @@ async def test_volumes_bind_mount_source(
679680
],
680681
)
681682

683+
@parameterized.expand([
684+
(
685+
"create_host_path_set_to_true",
686+
{"bind": {"create_host_path": True}},
687+
),
688+
(
689+
"create_host_path_default_true",
690+
{},
691+
),
692+
])
693+
async def test_volumes_bind_mount_create_source_dir(self, test_name: str, bind: dict) -> None:
694+
# creates a missing source dir
695+
c = create_compose_mock()
696+
c.prefer_volume_over_mount = True
697+
cnt = get_minimal_container()
698+
699+
cnt["_service"] = cnt["service_name"]
700+
701+
volume_info = {
702+
"type": "bind",
703+
"source": "./not_exists/foo",
704+
"target": "/mnt",
705+
}
706+
volume_info.update(bind)
707+
cnt["volumes"] = [
708+
volume_info,
709+
]
710+
711+
args = await container_to_args(c, cnt)
712+
713+
self.assertEqual(
714+
args,
715+
[
716+
"--name=project_name_service_name1",
717+
"-d",
718+
"-v",
719+
f"{get_test_file_path('./test_dirname/not_exists/foo')}:/mnt",
720+
"--network=bridge:alias=service_name",
721+
"busybox",
722+
],
723+
)
724+
dir_path = get_test_file_path('./test_dirname/not_exists/foo')
725+
shutil.rmtree(dir_path)
726+
727+
# throws an error as the source path does not exist and its creation was suppressed with the
728+
# create_host_path = False option
729+
async def test_volumes_bind_mount_source_does_not_exist(self) -> None:
730+
c = create_compose_mock()
731+
c.prefer_volume_over_mount = True
732+
cnt = get_minimal_container()
733+
734+
cnt["_service"] = cnt["service_name"]
735+
736+
cnt["volumes"] = [
737+
{
738+
"type": "bind",
739+
"source": "./not_exists/foo",
740+
"target": "/mnt",
741+
"bind": {"create_host_path": False},
742+
}
743+
]
744+
745+
with self.assertRaises(ValueError):
746+
await container_to_args(c, cnt)
747+
682748
@parameterized.expand([
683749
("not_compat", False, "test_project_name", "test_project_name_network1"),
684750
("compat_no_dash", True, "test_project_name", "test_project_name_network1"),

0 commit comments

Comments
 (0)