Merge pull request #1238 from uosis/env-var-override

Add support for providing x-podman settings using environment variables
This commit is contained in:
Povilas Kanapickas 2025-06-11 22:38:25 +03:00 committed by GitHub
commit 9cde3993f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 120 additions and 11 deletions

View File

@ -156,6 +156,9 @@ x-podman:
By default `default_net_name_compat` is `false`. This will change to `true` at some point and the By default `default_net_name_compat` is `false`. This will change to `true` at some point and the
setting will be removed. setting will be removed.
This setting can also be changed by setting `PODMAN_COMPOSE_DEFAULT_NET_NAME_COMPAT` environment
variable.
## Compatibility of default network behavior between docker-compose and podman-compose ## Compatibility of default network behavior between docker-compose and podman-compose
When there is no network defined (neither network-mode nor networks) in service, When there is no network defined (neither network-mode nor networks) in service,
@ -176,6 +179,9 @@ x-podman:
default_net_behavior_compat: true default_net_behavior_compat: true
``` ```
This setting can also be changed by setting `PODMAN_COMPOSE_DEFAULT_NET_BEHAVIOR_COMPAT` environment
variable.
## Custom pods management ## Custom pods management
Podman-compose can have containers in pods. This can be controlled by extension key x-podman in_pod. Podman-compose can have containers in pods. This can be controlled by extension key x-podman in_pod.
@ -195,6 +201,9 @@ x-podman:
in_pod: false in_pod: false
``` ```
This setting can also be changed by setting `PODMAN_COMPOSE_IN_POD` environment
variable.
It is also possible to override the default arguments for pod creation that are It is also possible to override the default arguments for pod creation that are
used when --pod-args is not passed on the command line: used when --pod-args is not passed on the command line:
```yml ```yml
@ -208,3 +217,6 @@ x-podman:
``` ```
When not set in docker-compose.yml or on the command line, the pod args default When not set in docker-compose.yml or on the command line, the pod args default
to `["--infra=false", "--share="]`. to `["--infra=false", "--share="]`.
This setting can also be changed by setting `PODMAN_COMPOSE_POD_ARGS` environment
variable.

View File

@ -0,0 +1 @@
- Add support for setting x-podman values using PODMAN_COMPOSE_* environment variables.

View File

@ -370,7 +370,9 @@ def default_network_name_for_project(compose: PodmanCompose, net: str, is_ext: A
assert compose.project_name is not None assert compose.project_name is not None
default_net_name_compat = compose.x_podman.get("default_net_name_compat", False) default_net_name_compat = compose.x_podman.get(
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 f"{compose.project_name.replace('-', '')}_{net}"
return f"{compose.project_name}_{net}" return f"{compose.project_name}_{net}"
@ -1968,6 +1970,12 @@ COMPOSE_DEFAULT_LS = [
class PodmanCompose: class PodmanCompose:
class XPodmanSettingKey(Enum):
DEFAULT_NET_NAME_COMPAT = "default_net_name_compat"
DEFAULT_NET_BEHAVIOR_COMPAT = "default_net_behavior_compat"
IN_POD = "in_pod"
POD_ARGS = "pod_args"
def __init__(self) -> None: def __init__(self) -> None:
self.podman: Podman self.podman: Podman
self.podman_version: str | None = None self.podman_version: str | None = None
@ -1988,7 +1996,7 @@ class PodmanCompose:
self.services: dict[str, Any] self.services: dict[str, Any]
self.all_services: set[Any] = set() self.all_services: set[Any] = set()
self.prefer_volume_over_mount = True self.prefer_volume_over_mount = True
self.x_podman: dict[str, Any] = {} self.x_podman: dict[PodmanCompose.XPodmanSettingKey, Any] = {}
self.merged_yaml: Any self.merged_yaml: Any
self.yaml_hash = "" self.yaml_hash = ""
self.console_colors = [ self.console_colors = [
@ -2059,7 +2067,7 @@ class PodmanCompose:
def resolve_in_pod(self) -> bool: def resolve_in_pod(self) -> bool:
if self.global_args.in_pod in (None, ''): if self.global_args.in_pod in (None, ''):
self.global_args.in_pod = self.x_podman.get("in_pod", "1") self.global_args.in_pod = self.x_podman.get(PodmanCompose.XPodmanSettingKey.IN_POD, "1")
# otherwise use `in_pod` value provided by command line # otherwise use `in_pod` value provided by command line
return self.global_args.in_pod return self.global_args.in_pod
@ -2070,7 +2078,43 @@ class PodmanCompose:
# - Default value # - Default value
if self.global_args.pod_args is not None: if self.global_args.pod_args is not None:
return shlex.split(self.global_args.pod_args) return shlex.split(self.global_args.pod_args)
return self.x_podman.get("pod_args", ["--infra=false", "--share="]) return self.x_podman.get(
PodmanCompose.XPodmanSettingKey.POD_ARGS, ["--infra=false", "--share="]
)
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}
self.x_podman = {}
for k, v in compose.get("x-podman", {}).items():
known_key = known_keys.get(k)
if known_key:
self.x_podman[known_key] = v
else:
log.warning(
"unknown x-podman key [%s] in compose file, supported keys: %s",
k,
", ".join(known_keys.keys()),
)
env = {
key.removeprefix("PODMAN_COMPOSE_").lower(): value
for key, value in environ.items()
if key.startswith("PODMAN_COMPOSE_")
and key not in {"PODMAN_COMPOSE_PROVIDER", "PODMAN_COMPOSE_WARNING_LOGS"}
}
for k, v in env.items():
known_key = known_keys.get(k)
if known_key:
self.x_podman[known_key] = v
else:
log.warning(
"unknown PODMAN_COMPOSE_ key [%s] in environment, supported keys: %s",
k,
", ".join(known_keys.keys()),
)
def _parse_compose_file(self) -> None: def _parse_compose_file(self) -> None:
args = self.global_args args = self.global_args
@ -2219,6 +2263,8 @@ class PodmanCompose:
log.debug(" ** merged:\n%s", json.dumps(compose, indent=2)) log.debug(" ** merged:\n%s", json.dumps(compose, indent=2))
# ver = compose.get('version') # ver = compose.get('version')
self._parse_x_podman_settings(compose, self.environ)
services: dict | None = compose.get("services") services: dict | None = compose.get("services")
if services is None: if services is None:
services = {} services = {}
@ -2236,7 +2282,7 @@ class PodmanCompose:
nets["default"] = None nets["default"] = None
self.networks = nets self.networks = nets
if compose.get("x-podman", {}).get("default_net_behavior_compat", False): if self.x_podman.get(PodmanCompose.XPodmanSettingKey.DEFAULT_NET_BEHAVIOR_COMPAT, False):
# If there is no network_mode and networks in service, # If there is no network_mode and networks in service,
# docker-compose will create default network named '<project_name>_default' # docker-compose will create default network named '<project_name>_default'
# and add the service to the default network. # and add the service to the default network.
@ -2353,8 +2399,6 @@ 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])
self.x_podman = compose.get("x-podman", {})
args.in_pod = self.resolve_in_pod() args.in_pod = self.resolve_in_pod()
args.pod_arg_list = self.resolve_pod_args() args.pod_arg_list = self.resolve_pod_args()
pods, containers = transform(args, project_name, given_containers) pods, containers = transform(args, project_name, given_containers)

View File

@ -467,6 +467,57 @@ class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin):
# can not actually find this pod because it was not created # can not actually find this pod because it was not created
self.run_subprocess_assert_returncode(command_rm_pod, 1) self.run_subprocess_assert_returncode(command_rm_pod, 1)
def test_x_podman_in_pod_not_exists_command_line_in_pod_not_exists_env_var(self) -> None:
"""
Test that podman-compose will not create a pod when env var is set.
"""
command_up = [
"python3",
os.path.join(base_path(), "podman_compose.py"),
"-f",
os.path.join(
base_path(),
"tests",
"integration",
"in_pod",
"custom_x-podman_not_exists",
"docker-compose.yml",
),
"up",
"-d",
]
down_cmd = [
"python3",
podman_compose_path(),
"-f",
os.path.join(
base_path(),
"tests",
"integration",
"in_pod",
"custom_x-podman_not_exists",
"docker-compose.yml",
),
"down",
]
env = {
"PODMAN_COMPOSE_IN_POD": "0",
}
try:
self.run_subprocess_assert_returncode(
command_up, failure_exitcode_when_rootful(), env=env
)
finally:
self.run_subprocess_assert_returncode(down_cmd, env=env)
command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_not_exists"]
# can not actually find this pod because it was not created
self.run_subprocess_assert_returncode(command_rm_pod, 1)
def test_x_podman_in_pod_custom_name(self) -> None: def test_x_podman_in_pod_custom_name(self) -> None:
""" """
Test that podman-compose will create a pod with a custom name Test that podman-compose will create a pod with a custom name

View File

@ -34,7 +34,7 @@ class RunSubprocessMixin:
def is_debug_enabled(self) -> bool: def is_debug_enabled(self) -> bool:
return "TESTS_DEBUG" in os.environ return "TESTS_DEBUG" in os.environ
def run_subprocess(self, args: list[str]) -> tuple[bytes, bytes, int]: def run_subprocess(self, args: list[str], env: dict[str, str] = {}) -> tuple[bytes, bytes, int]:
begin = time.time() begin = time.time()
if self.is_debug_enabled(): if self.is_debug_enabled():
print("TEST_CALL", args) print("TEST_CALL", args)
@ -42,6 +42,7 @@ class RunSubprocessMixin:
args, args,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
env=os.environ | env,
) )
out, err = proc.communicate() out, err = proc.communicate()
if self.is_debug_enabled(): if self.is_debug_enabled():
@ -51,9 +52,9 @@ class RunSubprocessMixin:
return out, err, proc.returncode return out, err, proc.returncode
def run_subprocess_assert_returncode( def run_subprocess_assert_returncode(
self, args: list[str], expected_returncode: int = 0 self, args: list[str], expected_returncode: int = 0, env: dict[str, str] = {}
) -> tuple[bytes, bytes]: ) -> tuple[bytes, bytes]:
out, err, returncode = self.run_subprocess(args) out, err, returncode = self.run_subprocess(args, env=env)
decoded_out = out.decode('utf-8') decoded_out = out.decode('utf-8')
decoded_err = err.decode('utf-8') decoded_err = err.decode('utf-8')
self.assertEqual( # type: ignore[attr-defined] self.assertEqual( # type: ignore[attr-defined]

View File

@ -650,7 +650,7 @@ class TestContainerToArgs(unittest.IsolatedAsyncioTestCase):
self, name: str, is_compat: bool, project_name: str, expected_network_name: str self, name: str, is_compat: bool, project_name: str, expected_network_name: str
) -> None: ) -> None:
c = create_compose_mock(project_name) c = create_compose_mock(project_name)
c.x_podman = {"default_net_name_compat": is_compat} c.x_podman = {PodmanCompose.XPodmanSettingKey.DEFAULT_NET_NAME_COMPAT: is_compat}
c.networks = {'network1': {}} c.networks = {'network1': {}}
cnt = get_minimal_container() cnt = get_minimal_container()