mirror of
https://github.com/containers/podman-compose.git
synced 2025-06-20 03:37:47 +02:00
Merge pull request #1241 from uosis/name-separator
Add support for using hyphens for name separation
This commit is contained in:
commit
fa2252801a
@ -139,6 +139,26 @@ The options to the network modes are passed to the `--network` option of the `po
|
|||||||
as-is.
|
as-is.
|
||||||
|
|
||||||
|
|
||||||
|
## Compatibility of name separators between docker-compose and podman-compose
|
||||||
|
|
||||||
|
Currently, podman-compose is using underscores (`_` character) as a separator in names of
|
||||||
|
containers, images, etc., while docker-compose has switched to hyphens (`-` character). This setting
|
||||||
|
allows to switch podman-compose to use hyphens as well.
|
||||||
|
|
||||||
|
To enable compatibility between docker-compose and podman-compose, specify
|
||||||
|
`name_separator_compat: true` under global `x-podman` key:
|
||||||
|
|
||||||
|
```
|
||||||
|
x-podman:
|
||||||
|
name_separator_compat: true
|
||||||
|
```
|
||||||
|
|
||||||
|
By default `name_separator_compat` is `false`. This will change to `true` at some point and the
|
||||||
|
setting will be removed.
|
||||||
|
|
||||||
|
This setting can also be changed by setting `PODMAN_COMPOSE_NAME_SEPARATOR_COMPAT` environment
|
||||||
|
variable.
|
||||||
|
|
||||||
## Compatibility of default network names between docker-compose and podman-compose
|
## Compatibility of default network names between docker-compose and podman-compose
|
||||||
|
|
||||||
Current versions of podman-compose may produce different default external network names than
|
Current versions of podman-compose may produce different default external network names than
|
||||||
|
1
newsfragments/name-separator-compat.feature
Normal file
1
newsfragments/name-separator-compat.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
- Add new name_separator_compat x-podman setting to change name separator to hyphen, same as Docker Compose
|
@ -223,11 +223,10 @@ def fix_mount_dict(
|
|||||||
# handle anonymous or implied volume
|
# handle anonymous or implied volume
|
||||||
if not source:
|
if not source:
|
||||||
# missing source
|
# missing source
|
||||||
vol["name"] = "_".join([
|
vol["name"] = compose.format_name(
|
||||||
compose.project_name,
|
|
||||||
srv_name,
|
srv_name,
|
||||||
hashlib.sha256(mount_dict["target"].encode("utf-8")).hexdigest(),
|
hashlib.sha256(mount_dict["target"].encode("utf-8")).hexdigest(),
|
||||||
])
|
)
|
||||||
elif not name:
|
elif not name:
|
||||||
external = vol.get("external")
|
external = vol.get("external")
|
||||||
if isinstance(external, dict):
|
if isinstance(external, dict):
|
||||||
@ -374,38 +373,25 @@ def default_network_name_for_project(compose: PodmanCompose, net: str, is_ext: A
|
|||||||
PodmanCompose.XPodmanSettingKey.DEFAULT_NET_NAME_COMPAT, False
|
PodmanCompose.XPodmanSettingKey.DEFAULT_NET_NAME_COMPAT, False
|
||||||
)
|
)
|
||||||
if default_net_name_compat is True:
|
if default_net_name_compat is True:
|
||||||
return f"{compose.project_name.replace('-', '')}_{net}"
|
return compose.join_name_parts(compose.project_name.replace('-', ''), net)
|
||||||
return f"{compose.project_name}_{net}"
|
return compose.format_name(net)
|
||||||
|
|
||||||
|
|
||||||
# def tr_identity(project_name, given_containers):
|
def try_parse_bool(value: Any) -> bool | None:
|
||||||
# pod_name = f'pod_{project_name}'
|
if isinstance(value, bool):
|
||||||
# pod = dict(name=pod_name)
|
return value
|
||||||
# containers = []
|
if isinstance(value, str):
|
||||||
# for cnt in given_containers:
|
value = value.lower()
|
||||||
# containers.append(dict(cnt, pod=pod_name))
|
if value in ('true', '1'):
|
||||||
# return [pod], containers
|
return True
|
||||||
|
if value in ('false', '0'):
|
||||||
|
return False
|
||||||
def transform(
|
if isinstance(value, int):
|
||||||
args: Any, project_name: str, given_containers: list[Any]
|
if value == 1:
|
||||||
) -> tuple[list[dict], list[dict]]:
|
return True
|
||||||
in_pod = str(args.in_pod).lower()
|
if value == 0:
|
||||||
pod_name = None
|
return False
|
||||||
pods = []
|
return None
|
||||||
|
|
||||||
if in_pod in ('true', '1', 'none', ''):
|
|
||||||
pod_name = f"pod_{project_name}"
|
|
||||||
elif in_pod not in ('false', '0'):
|
|
||||||
pod_name = args.in_pod
|
|
||||||
|
|
||||||
if pod_name:
|
|
||||||
pods = [{"name": pod_name}]
|
|
||||||
|
|
||||||
containers = []
|
|
||||||
for cnt in given_containers:
|
|
||||||
containers.append(dict(cnt, pod=pod_name))
|
|
||||||
return pods, containers
|
|
||||||
|
|
||||||
|
|
||||||
async def assert_volume(compose: PodmanCompose, mount_dict: dict[str, Any]) -> None:
|
async def assert_volume(compose: PodmanCompose, mount_dict: dict[str, Any]) -> None:
|
||||||
@ -1973,6 +1959,7 @@ class PodmanCompose:
|
|||||||
class XPodmanSettingKey(Enum):
|
class XPodmanSettingKey(Enum):
|
||||||
DEFAULT_NET_NAME_COMPAT = "default_net_name_compat"
|
DEFAULT_NET_NAME_COMPAT = "default_net_name_compat"
|
||||||
DEFAULT_NET_BEHAVIOR_COMPAT = "default_net_behavior_compat"
|
DEFAULT_NET_BEHAVIOR_COMPAT = "default_net_behavior_compat"
|
||||||
|
NAME_SEPARATOR_COMPAT = "name_separator_compat"
|
||||||
IN_POD = "in_pod"
|
IN_POD = "in_pod"
|
||||||
POD_ARGS = "pod_args"
|
POD_ARGS = "pod_args"
|
||||||
|
|
||||||
@ -2065,11 +2052,23 @@ class PodmanCompose:
|
|||||||
if isinstance(retcode, int):
|
if isinstance(retcode, int):
|
||||||
sys.exit(retcode)
|
sys.exit(retcode)
|
||||||
|
|
||||||
def resolve_in_pod(self) -> bool:
|
def resolve_pod_name(self) -> str | None:
|
||||||
if self.global_args.in_pod in (None, ''):
|
# Priorities:
|
||||||
self.global_args.in_pod = self.x_podman.get(PodmanCompose.XPodmanSettingKey.IN_POD, "1")
|
# - Command line --in-pod
|
||||||
# otherwise use `in_pod` value provided by command line
|
# - docker-compose.yml x-podman.in_pod
|
||||||
return self.global_args.in_pod
|
# - Default value of true
|
||||||
|
in_pod_arg = self.global_args.in_pod or self.x_podman.get(
|
||||||
|
PodmanCompose.XPodmanSettingKey.IN_POD, True
|
||||||
|
)
|
||||||
|
|
||||||
|
in_pod_arg_parsed = try_parse_bool(in_pod_arg)
|
||||||
|
if in_pod_arg_parsed is True:
|
||||||
|
return f"pod_{self.project_name}"
|
||||||
|
if in_pod_arg_parsed is False:
|
||||||
|
return None
|
||||||
|
|
||||||
|
assert isinstance(in_pod_arg, str) and in_pod_arg
|
||||||
|
return in_pod_arg
|
||||||
|
|
||||||
def resolve_pod_args(self) -> list[str]:
|
def resolve_pod_args(self) -> list[str]:
|
||||||
# Priorities:
|
# Priorities:
|
||||||
@ -2082,6 +2081,18 @@ class PodmanCompose:
|
|||||||
PodmanCompose.XPodmanSettingKey.POD_ARGS, ["--infra=false", "--share="]
|
PodmanCompose.XPodmanSettingKey.POD_ARGS, ["--infra=false", "--share="]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def join_name_parts(self, *parts: str) -> str:
|
||||||
|
setting = self.x_podman.get(PodmanCompose.XPodmanSettingKey.NAME_SEPARATOR_COMPAT, False)
|
||||||
|
if try_parse_bool(setting):
|
||||||
|
sep = "-"
|
||||||
|
else:
|
||||||
|
sep = "_"
|
||||||
|
return sep.join(parts)
|
||||||
|
|
||||||
|
def format_name(self, *parts: str) -> str:
|
||||||
|
assert self.project_name is not None
|
||||||
|
return self.join_name_parts(self.project_name, *parts)
|
||||||
|
|
||||||
def _parse_x_podman_settings(self, compose: dict[str, Any], environ: dict[str, str]) -> None:
|
def _parse_x_podman_settings(self, compose: dict[str, Any], environ: dict[str, str]) -> None:
|
||||||
known_keys = {s.value: s for s in PodmanCompose.XPodmanSettingKey}
|
known_keys = {s.value: s for s in PodmanCompose.XPodmanSettingKey}
|
||||||
|
|
||||||
@ -2265,6 +2276,8 @@ class PodmanCompose:
|
|||||||
|
|
||||||
self._parse_x_podman_settings(compose, self.environ)
|
self._parse_x_podman_settings(compose, self.environ)
|
||||||
|
|
||||||
|
pod_name = self.resolve_pod_name()
|
||||||
|
|
||||||
services: dict | None = compose.get("services")
|
services: dict | None = compose.get("services")
|
||||||
if services is None:
|
if services is None:
|
||||||
services = {}
|
services = {}
|
||||||
@ -2352,7 +2365,7 @@ class PodmanCompose:
|
|||||||
|
|
||||||
container_names_by_service[service_name] = []
|
container_names_by_service[service_name] = []
|
||||||
for num in range(1, replicas + 1):
|
for num in range(1, replicas + 1):
|
||||||
name0 = f"{project_name}_{service_name}_{num}"
|
name0 = self.format_name(service_name, str(num))
|
||||||
if num == 1:
|
if num == 1:
|
||||||
name = service_desc.get("container_name", name0)
|
name = service_desc.get("container_name", name0)
|
||||||
else:
|
else:
|
||||||
@ -2360,6 +2373,7 @@ class PodmanCompose:
|
|||||||
container_names_by_service[service_name].append(name)
|
container_names_by_service[service_name].append(name)
|
||||||
# log(service_name,service_desc)
|
# log(service_name,service_desc)
|
||||||
cnt = {
|
cnt = {
|
||||||
|
"pod": pod_name,
|
||||||
"name": name,
|
"name": name,
|
||||||
"num": num,
|
"num": num,
|
||||||
"service_name": service_name,
|
"service_name": service_name,
|
||||||
@ -2368,7 +2382,7 @@ class PodmanCompose:
|
|||||||
x_podman = service_desc.get("x-podman")
|
x_podman = service_desc.get("x-podman")
|
||||||
rootfs_mode = x_podman is not None and x_podman.get("rootfs") is not None
|
rootfs_mode = x_podman is not None and x_podman.get("rootfs") is not None
|
||||||
if "image" not in cnt and not rootfs_mode:
|
if "image" not in cnt and not rootfs_mode:
|
||||||
cnt["image"] = f"{project_name}_{service_name}"
|
cnt["image"] = self.format_name(service_name)
|
||||||
labels = norm_as_list(cnt.get("labels"))
|
labels = norm_as_list(cnt.get("labels"))
|
||||||
cnt["ports"] = norm_ports(cnt.get("ports"))
|
cnt["ports"] = norm_ports(cnt.get("ports"))
|
||||||
labels.extend(podman_compose_labels)
|
labels.extend(podman_compose_labels)
|
||||||
@ -2399,12 +2413,9 @@ class PodmanCompose:
|
|||||||
given_containers.sort(key=lambda c: len(c.get("_deps", [])))
|
given_containers.sort(key=lambda c: len(c.get("_deps", [])))
|
||||||
# log("sorted:", [c["name"] for c in given_containers])
|
# log("sorted:", [c["name"] for c in given_containers])
|
||||||
|
|
||||||
args.in_pod = self.resolve_in_pod()
|
self.pods = [{"name": pod_name}] if pod_name else []
|
||||||
args.pod_arg_list = self.resolve_pod_args()
|
self.containers = given_containers
|
||||||
pods, containers = transform(args, project_name, given_containers)
|
self.container_by_name = {c["name"]: c for c in given_containers}
|
||||||
self.pods = pods
|
|
||||||
self.containers = containers
|
|
||||||
self.container_by_name = {c["name"]: c for c in containers}
|
|
||||||
|
|
||||||
def _resolve_profiles(
|
def _resolve_profiles(
|
||||||
self, defined_services: dict[str, Any], requested_profiles: set[str] | None = None
|
self, defined_services: dict[str, Any], requested_profiles: set[str] | None = None
|
||||||
@ -2931,7 +2942,7 @@ async def pod_exists(compose: PodmanCompose, name: str) -> bool:
|
|||||||
return exit_code == 0
|
return exit_code == 0
|
||||||
|
|
||||||
|
|
||||||
async def create_pods(compose: PodmanCompose, args: argparse.Namespace) -> None:
|
async def create_pods(compose: PodmanCompose) -> None:
|
||||||
for pod in compose.pods:
|
for pod in compose.pods:
|
||||||
if await pod_exists(compose, pod["name"]):
|
if await pod_exists(compose, pod["name"]):
|
||||||
continue
|
continue
|
||||||
@ -2939,9 +2950,8 @@ async def create_pods(compose: PodmanCompose, args: argparse.Namespace) -> None:
|
|||||||
podman_args = [
|
podman_args = [
|
||||||
"create",
|
"create",
|
||||||
"--name=" + pod["name"],
|
"--name=" + pod["name"],
|
||||||
] + args.pod_arg_list
|
] + compose.resolve_pod_args()
|
||||||
# if compose.podman_version and not strverscmp_lt(compose.podman_version, "3.4.0"):
|
|
||||||
# podman_args.append("--infra-name={}_infra".format(pod["name"]))
|
|
||||||
ports = pod.get("ports", [])
|
ports = pod.get("ports", [])
|
||||||
if isinstance(ports, str):
|
if isinstance(ports, str):
|
||||||
ports = [ports]
|
ports = [ports]
|
||||||
@ -3074,7 +3084,7 @@ async def compose_up(compose: PodmanCompose, args: argparse.Namespace) -> int |
|
|||||||
log.info("recreating: done\n\n")
|
log.info("recreating: done\n\n")
|
||||||
# args.no_recreate disables check for changes (which is not implemented)
|
# args.no_recreate disables check for changes (which is not implemented)
|
||||||
|
|
||||||
await create_pods(compose, args)
|
await create_pods(compose)
|
||||||
exit_code = 0
|
exit_code = 0
|
||||||
for cnt in compose.containers:
|
for cnt in compose.containers:
|
||||||
if cnt["_service"] in excluded:
|
if cnt["_service"] in excluded:
|
||||||
@ -3318,7 +3328,7 @@ async def compose_ps(compose: PodmanCompose, args: argparse.Namespace) -> None:
|
|||||||
"create a container similar to a service to run a one-off command",
|
"create a container similar to a service to run a one-off command",
|
||||||
)
|
)
|
||||||
async def compose_run(compose: PodmanCompose, args: argparse.Namespace) -> None:
|
async def compose_run(compose: PodmanCompose, args: argparse.Namespace) -> None:
|
||||||
await create_pods(compose, args)
|
await create_pods(compose)
|
||||||
compose.assert_services(args.service)
|
compose.assert_services(args.service)
|
||||||
container_names = compose.container_names_by_service[args.service]
|
container_names = compose.container_names_by_service[args.service]
|
||||||
container_name = container_names[0]
|
container_name = container_names[0]
|
||||||
@ -3363,7 +3373,7 @@ def compose_run_update_container_from_args(
|
|||||||
compose: PodmanCompose, cnt: dict, args: argparse.Namespace
|
compose: PodmanCompose, cnt: dict, args: argparse.Namespace
|
||||||
) -> None:
|
) -> None:
|
||||||
# adjust one-off container options
|
# adjust one-off container options
|
||||||
name0 = "{}_{}_tmp{}".format(compose.project_name, args.service, random.randrange(0, 65536))
|
name0 = compose.format_name(args.service, f'tmp{random.randrange(0, 65536)}')
|
||||||
cnt["name"] = args.name or name0
|
cnt["name"] = args.name or name0
|
||||||
if args.entrypoint:
|
if args.entrypoint:
|
||||||
cnt["entrypoint"] = args.entrypoint
|
cnt["entrypoint"] = args.entrypoint
|
||||||
|
1
tests/integration/name_separator_compat/__init__.py
Normal file
1
tests/integration/name_separator_compat/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
@ -0,0 +1,7 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: busybox
|
||||||
|
command: httpd -f -p 8123 -h /tmp/
|
||||||
|
|
||||||
|
x-podman:
|
||||||
|
name_separator_compat: true
|
@ -0,0 +1,4 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: busybox
|
||||||
|
command: httpd -f -p 8123 -h /tmp/
|
@ -0,0 +1,59 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-2.0
|
||||||
|
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from parameterized import parameterized
|
||||||
|
|
||||||
|
from tests.integration.test_utils import RunSubprocessMixin
|
||||||
|
from tests.integration.test_utils import podman_compose_path
|
||||||
|
from tests.integration.test_utils import test_path
|
||||||
|
|
||||||
|
|
||||||
|
class TestComposeNameSeparatorCompat(unittest.TestCase, RunSubprocessMixin):
|
||||||
|
@parameterized.expand([
|
||||||
|
('default', {}, '_'),
|
||||||
|
('default', {'PODMAN_COMPOSE_NAME_SEPARATOR_COMPAT': '1'}, '-'),
|
||||||
|
('compat', {}, '-'),
|
||||||
|
('compat', {'PODMAN_COMPOSE_NAME_SEPARATOR_COMPAT': '1'}, '-'),
|
||||||
|
('compat', {'PODMAN_COMPOSE_NAME_SEPARATOR_COMPAT': '0'}, '_'),
|
||||||
|
])
|
||||||
|
def test_container_name(self, file: str, env: dict[str, str], expected_sep: str) -> None:
|
||||||
|
compose_yaml_path = os.path.join(
|
||||||
|
test_path(), "name_separator_compat", f"docker-compose_{file}.yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.run_subprocess_assert_returncode(
|
||||||
|
[podman_compose_path(), "-f", compose_yaml_path, "up", "-d"],
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
|
||||||
|
container_name_out, _ = self.run_subprocess_assert_returncode(
|
||||||
|
[
|
||||||
|
podman_compose_path(),
|
||||||
|
"-f",
|
||||||
|
compose_yaml_path,
|
||||||
|
"ps",
|
||||||
|
"--format",
|
||||||
|
'{{.Names}}',
|
||||||
|
],
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
container_name = container_name_out.decode('utf-8').strip()
|
||||||
|
|
||||||
|
expected_container_name = f'name_separator_compat{expected_sep}web{expected_sep}1'
|
||||||
|
|
||||||
|
self.assertEqual(container_name, expected_container_name)
|
||||||
|
finally:
|
||||||
|
self.run_subprocess_assert_returncode(
|
||||||
|
[
|
||||||
|
podman_compose_path(),
|
||||||
|
"-f",
|
||||||
|
compose_yaml_path,
|
||||||
|
"down",
|
||||||
|
"-t",
|
||||||
|
"0",
|
||||||
|
],
|
||||||
|
env=env,
|
||||||
|
)
|
@ -56,7 +56,9 @@ def get_minimal_container() -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def get_minimal_compose() -> PodmanCompose:
|
def get_minimal_compose() -> PodmanCompose:
|
||||||
return PodmanCompose()
|
compose = PodmanCompose()
|
||||||
|
compose.project_name = "test_project"
|
||||||
|
return compose
|
||||||
|
|
||||||
|
|
||||||
def get_minimal_args() -> argparse.Namespace:
|
def get_minimal_args() -> argparse.Namespace:
|
||||||
@ -67,7 +69,7 @@ def get_minimal_args() -> argparse.Namespace:
|
|||||||
env=None,
|
env=None,
|
||||||
name="default_name",
|
name="default_name",
|
||||||
rm=None,
|
rm=None,
|
||||||
service=None,
|
service="test_service",
|
||||||
publish=None,
|
publish=None,
|
||||||
service_ports=None,
|
service_ports=None,
|
||||||
user=None,
|
user=None,
|
||||||
|
@ -20,6 +20,8 @@ def create_compose_mock(project_name: str = "test_project_name") -> PodmanCompos
|
|||||||
compose.default_net = None
|
compose.default_net = None
|
||||||
compose.networks = {}
|
compose.networks = {}
|
||||||
compose.x_podman = {}
|
compose.x_podman = {}
|
||||||
|
compose.join_name_parts = mock.Mock(side_effect=lambda *args: '_'.join(args))
|
||||||
|
compose.format_name = mock.Mock(side_effect=lambda *args: '_'.join([project_name, *args]))
|
||||||
|
|
||||||
async def podman_output(*args: Any, **kwargs: Any) -> None:
|
async def podman_output(*args: Any, **kwargs: Any) -> None:
|
||||||
pass
|
pass
|
||||||
|
Loading…
x
Reference in New Issue
Block a user