Skip to content

Commit fa22528

Browse files
authored
Merge pull request #1241 from uosis/name-separator
Add support for using hyphens for name separation
2 parents 9cde399 + 6635b8b commit fa22528

File tree

9 files changed

+161
-55
lines changed

9 files changed

+161
-55
lines changed

docs/Extensions.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,26 @@ The options to the network modes are passed to the `--network` option of the `po
139139
as-is.
140140

141141

142+
## Compatibility of name separators between docker-compose and podman-compose
143+
144+
Currently, podman-compose is using underscores (`_` character) as a separator in names of
145+
containers, images, etc., while docker-compose has switched to hyphens (`-` character). This setting
146+
allows to switch podman-compose to use hyphens as well.
147+
148+
To enable compatibility between docker-compose and podman-compose, specify
149+
`name_separator_compat: true` under global `x-podman` key:
150+
151+
```
152+
x-podman:
153+
name_separator_compat: true
154+
```
155+
156+
By default `name_separator_compat` is `false`. This will change to `true` at some point and the
157+
setting will be removed.
158+
159+
This setting can also be changed by setting `PODMAN_COMPOSE_NAME_SEPARATOR_COMPAT` environment
160+
variable.
161+
142162
## Compatibility of default network names between docker-compose and podman-compose
143163

144164
Current versions of podman-compose may produce different default external network names than
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Add new name_separator_compat x-podman setting to change name separator to hyphen, same as Docker Compose

podman_compose.py

Lines changed: 63 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -223,11 +223,10 @@ def fix_mount_dict(
223223
# handle anonymous or implied volume
224224
if not source:
225225
# missing source
226-
vol["name"] = "_".join([
227-
compose.project_name,
226+
vol["name"] = compose.format_name(
228227
srv_name,
229228
hashlib.sha256(mount_dict["target"].encode("utf-8")).hexdigest(),
230-
])
229+
)
231230
elif not name:
232231
external = vol.get("external")
233232
if isinstance(external, dict):
@@ -374,38 +373,25 @@ def default_network_name_for_project(compose: PodmanCompose, net: str, is_ext: A
374373
PodmanCompose.XPodmanSettingKey.DEFAULT_NET_NAME_COMPAT, False
375374
)
376375
if default_net_name_compat is True:
377-
return f"{compose.project_name.replace('-', '')}_{net}"
378-
return f"{compose.project_name}_{net}"
379-
380-
381-
# def tr_identity(project_name, given_containers):
382-
# pod_name = f'pod_{project_name}'
383-
# pod = dict(name=pod_name)
384-
# containers = []
385-
# for cnt in given_containers:
386-
# containers.append(dict(cnt, pod=pod_name))
387-
# return [pod], containers
388-
376+
return compose.join_name_parts(compose.project_name.replace('-', ''), net)
377+
return compose.format_name(net)
389378

390-
def transform(
391-
args: Any, project_name: str, given_containers: list[Any]
392-
) -> tuple[list[dict], list[dict]]:
393-
in_pod = str(args.in_pod).lower()
394-
pod_name = None
395-
pods = []
396379

397-
if in_pod in ('true', '1', 'none', ''):
398-
pod_name = f"pod_{project_name}"
399-
elif in_pod not in ('false', '0'):
400-
pod_name = args.in_pod
401-
402-
if pod_name:
403-
pods = [{"name": pod_name}]
404-
405-
containers = []
406-
for cnt in given_containers:
407-
containers.append(dict(cnt, pod=pod_name))
408-
return pods, containers
380+
def try_parse_bool(value: Any) -> bool | None:
381+
if isinstance(value, bool):
382+
return value
383+
if isinstance(value, str):
384+
value = value.lower()
385+
if value in ('true', '1'):
386+
return True
387+
if value in ('false', '0'):
388+
return False
389+
if isinstance(value, int):
390+
if value == 1:
391+
return True
392+
if value == 0:
393+
return False
394+
return None
409395

410396

411397
async def assert_volume(compose: PodmanCompose, mount_dict: dict[str, Any]) -> None:
@@ -1973,6 +1959,7 @@ class PodmanCompose:
19731959
class XPodmanSettingKey(Enum):
19741960
DEFAULT_NET_NAME_COMPAT = "default_net_name_compat"
19751961
DEFAULT_NET_BEHAVIOR_COMPAT = "default_net_behavior_compat"
1962+
NAME_SEPARATOR_COMPAT = "name_separator_compat"
19761963
IN_POD = "in_pod"
19771964
POD_ARGS = "pod_args"
19781965

@@ -2065,11 +2052,23 @@ async def run(self, argv: list[str] | None = None) -> None:
20652052
if isinstance(retcode, int):
20662053
sys.exit(retcode)
20672054

2068-
def resolve_in_pod(self) -> bool:
2069-
if self.global_args.in_pod in (None, ''):
2070-
self.global_args.in_pod = self.x_podman.get(PodmanCompose.XPodmanSettingKey.IN_POD, "1")
2071-
# otherwise use `in_pod` value provided by command line
2072-
return self.global_args.in_pod
2055+
def resolve_pod_name(self) -> str | None:
2056+
# Priorities:
2057+
# - Command line --in-pod
2058+
# - docker-compose.yml x-podman.in_pod
2059+
# - Default value of true
2060+
in_pod_arg = self.global_args.in_pod or self.x_podman.get(
2061+
PodmanCompose.XPodmanSettingKey.IN_POD, True
2062+
)
2063+
2064+
in_pod_arg_parsed = try_parse_bool(in_pod_arg)
2065+
if in_pod_arg_parsed is True:
2066+
return f"pod_{self.project_name}"
2067+
if in_pod_arg_parsed is False:
2068+
return None
2069+
2070+
assert isinstance(in_pod_arg, str) and in_pod_arg
2071+
return in_pod_arg
20732072

20742073
def resolve_pod_args(self) -> list[str]:
20752074
# Priorities:
@@ -2082,6 +2081,18 @@ def resolve_pod_args(self) -> list[str]:
20822081
PodmanCompose.XPodmanSettingKey.POD_ARGS, ["--infra=false", "--share="]
20832082
)
20842083

2084+
def join_name_parts(self, *parts: str) -> str:
2085+
setting = self.x_podman.get(PodmanCompose.XPodmanSettingKey.NAME_SEPARATOR_COMPAT, False)
2086+
if try_parse_bool(setting):
2087+
sep = "-"
2088+
else:
2089+
sep = "_"
2090+
return sep.join(parts)
2091+
2092+
def format_name(self, *parts: str) -> str:
2093+
assert self.project_name is not None
2094+
return self.join_name_parts(self.project_name, *parts)
2095+
20852096
def _parse_x_podman_settings(self, compose: dict[str, Any], environ: dict[str, str]) -> None:
20862097
known_keys = {s.value: s for s in PodmanCompose.XPodmanSettingKey}
20872098

@@ -2265,6 +2276,8 @@ def _parse_compose_file(self) -> None:
22652276

22662277
self._parse_x_podman_settings(compose, self.environ)
22672278

2279+
pod_name = self.resolve_pod_name()
2280+
22682281
services: dict | None = compose.get("services")
22692282
if services is None:
22702283
services = {}
@@ -2352,14 +2365,15 @@ def _parse_compose_file(self) -> None:
23522365

23532366
container_names_by_service[service_name] = []
23542367
for num in range(1, replicas + 1):
2355-
name0 = f"{project_name}_{service_name}_{num}"
2368+
name0 = self.format_name(service_name, str(num))
23562369
if num == 1:
23572370
name = service_desc.get("container_name", name0)
23582371
else:
23592372
name = name0
23602373
container_names_by_service[service_name].append(name)
23612374
# log(service_name,service_desc)
23622375
cnt = {
2376+
"pod": pod_name,
23632377
"name": name,
23642378
"num": num,
23652379
"service_name": service_name,
@@ -2368,7 +2382,7 @@ def _parse_compose_file(self) -> None:
23682382
x_podman = service_desc.get("x-podman")
23692383
rootfs_mode = x_podman is not None and x_podman.get("rootfs") is not None
23702384
if "image" not in cnt and not rootfs_mode:
2371-
cnt["image"] = f"{project_name}_{service_name}"
2385+
cnt["image"] = self.format_name(service_name)
23722386
labels = norm_as_list(cnt.get("labels"))
23732387
cnt["ports"] = norm_ports(cnt.get("ports"))
23742388
labels.extend(podman_compose_labels)
@@ -2399,12 +2413,9 @@ def _parse_compose_file(self) -> None:
23992413
given_containers.sort(key=lambda c: len(c.get("_deps", [])))
24002414
# log("sorted:", [c["name"] for c in given_containers])
24012415

2402-
args.in_pod = self.resolve_in_pod()
2403-
args.pod_arg_list = self.resolve_pod_args()
2404-
pods, containers = transform(args, project_name, given_containers)
2405-
self.pods = pods
2406-
self.containers = containers
2407-
self.container_by_name = {c["name"]: c for c in containers}
2416+
self.pods = [{"name": pod_name}] if pod_name else []
2417+
self.containers = given_containers
2418+
self.container_by_name = {c["name"]: c for c in given_containers}
24082419

24092420
def _resolve_profiles(
24102421
self, defined_services: dict[str, Any], requested_profiles: set[str] | None = None
@@ -2931,17 +2942,16 @@ async def pod_exists(compose: PodmanCompose, name: str) -> bool:
29312942
return exit_code == 0
29322943

29332944

2934-
async def create_pods(compose: PodmanCompose, args: argparse.Namespace) -> None:
2945+
async def create_pods(compose: PodmanCompose) -> None:
29352946
for pod in compose.pods:
29362947
if await pod_exists(compose, pod["name"]):
29372948
continue
29382949

29392950
podman_args = [
29402951
"create",
29412952
"--name=" + pod["name"],
2942-
] + args.pod_arg_list
2943-
# if compose.podman_version and not strverscmp_lt(compose.podman_version, "3.4.0"):
2944-
# podman_args.append("--infra-name={}_infra".format(pod["name"]))
2953+
] + compose.resolve_pod_args()
2954+
29452955
ports = pod.get("ports", [])
29462956
if isinstance(ports, str):
29472957
ports = [ports]
@@ -3074,7 +3084,7 @@ async def compose_up(compose: PodmanCompose, args: argparse.Namespace) -> int |
30743084
log.info("recreating: done\n\n")
30753085
# args.no_recreate disables check for changes (which is not implemented)
30763086

3077-
await create_pods(compose, args)
3087+
await create_pods(compose)
30783088
exit_code = 0
30793089
for cnt in compose.containers:
30803090
if cnt["_service"] in excluded:
@@ -3318,7 +3328,7 @@ async def compose_ps(compose: PodmanCompose, args: argparse.Namespace) -> None:
33183328
"create a container similar to a service to run a one-off command",
33193329
)
33203330
async def compose_run(compose: PodmanCompose, args: argparse.Namespace) -> None:
3321-
await create_pods(compose, args)
3331+
await create_pods(compose)
33223332
compose.assert_services(args.service)
33233333
container_names = compose.container_names_by_service[args.service]
33243334
container_name = container_names[0]
@@ -3363,7 +3373,7 @@ def compose_run_update_container_from_args(
33633373
compose: PodmanCompose, cnt: dict, args: argparse.Namespace
33643374
) -> None:
33653375
# adjust one-off container options
3366-
name0 = "{}_{}_tmp{}".format(compose.project_name, args.service, random.randrange(0, 65536))
3376+
name0 = compose.format_name(args.service, f'tmp{random.randrange(0, 65536)}')
33673377
cnt["name"] = args.name or name0
33683378
if args.entrypoint:
33693379
cnt["entrypoint"] = args.entrypoint
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
services:
2+
web:
3+
image: busybox
4+
command: httpd -f -p 8123 -h /tmp/
5+
6+
x-podman:
7+
name_separator_compat: true
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
services:
2+
web:
3+
image: busybox
4+
command: httpd -f -p 8123 -h /tmp/
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# SPDX-License-Identifier: GPL-2.0
2+
3+
import os
4+
import unittest
5+
6+
from parameterized import parameterized
7+
8+
from tests.integration.test_utils import RunSubprocessMixin
9+
from tests.integration.test_utils import podman_compose_path
10+
from tests.integration.test_utils import test_path
11+
12+
13+
class TestComposeNameSeparatorCompat(unittest.TestCase, RunSubprocessMixin):
14+
@parameterized.expand([
15+
('default', {}, '_'),
16+
('default', {'PODMAN_COMPOSE_NAME_SEPARATOR_COMPAT': '1'}, '-'),
17+
('compat', {}, '-'),
18+
('compat', {'PODMAN_COMPOSE_NAME_SEPARATOR_COMPAT': '1'}, '-'),
19+
('compat', {'PODMAN_COMPOSE_NAME_SEPARATOR_COMPAT': '0'}, '_'),
20+
])
21+
def test_container_name(self, file: str, env: dict[str, str], expected_sep: str) -> None:
22+
compose_yaml_path = os.path.join(
23+
test_path(), "name_separator_compat", f"docker-compose_{file}.yaml"
24+
)
25+
26+
try:
27+
self.run_subprocess_assert_returncode(
28+
[podman_compose_path(), "-f", compose_yaml_path, "up", "-d"],
29+
env=env,
30+
)
31+
32+
container_name_out, _ = self.run_subprocess_assert_returncode(
33+
[
34+
podman_compose_path(),
35+
"-f",
36+
compose_yaml_path,
37+
"ps",
38+
"--format",
39+
'{{.Names}}',
40+
],
41+
env=env,
42+
)
43+
container_name = container_name_out.decode('utf-8').strip()
44+
45+
expected_container_name = f'name_separator_compat{expected_sep}web{expected_sep}1'
46+
47+
self.assertEqual(container_name, expected_container_name)
48+
finally:
49+
self.run_subprocess_assert_returncode(
50+
[
51+
podman_compose_path(),
52+
"-f",
53+
compose_yaml_path,
54+
"down",
55+
"-t",
56+
"0",
57+
],
58+
env=env,
59+
)

tests/unit/test_compose_run_update_container_from_args.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ def get_minimal_container() -> dict:
5656

5757

5858
def get_minimal_compose() -> PodmanCompose:
59-
return PodmanCompose()
59+
compose = PodmanCompose()
60+
compose.project_name = "test_project"
61+
return compose
6062

6163

6264
def get_minimal_args() -> argparse.Namespace:
@@ -67,7 +69,7 @@ def get_minimal_args() -> argparse.Namespace:
6769
env=None,
6870
name="default_name",
6971
rm=None,
70-
service=None,
72+
service="test_service",
7173
publish=None,
7274
service_ports=None,
7375
user=None,

tests/unit/test_container_to_args.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ def create_compose_mock(project_name: str = "test_project_name") -> PodmanCompos
2020
compose.default_net = None
2121
compose.networks = {}
2222
compose.x_podman = {}
23+
compose.join_name_parts = mock.Mock(side_effect=lambda *args: '_'.join(args))
24+
compose.format_name = mock.Mock(side_effect=lambda *args: '_'.join([project_name, *args]))
2325

2426
async def podman_output(*args: Any, **kwargs: Any) -> None:
2527
pass

0 commit comments

Comments
 (0)