diff --git a/newsfragments/x-podman-env-vars.feature b/newsfragments/x-podman-env-vars.feature new file mode 100644 index 0000000..d2e06f5 --- /dev/null +++ b/newsfragments/x-podman-env-vars.feature @@ -0,0 +1 @@ +- Add support for setting x-podman values using PODMAN_COMPOSE_* environment variables. \ No newline at end of file diff --git a/podman_compose.py b/podman_compose.py index 6f8e28d..2805d52 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -370,7 +370,9 @@ def default_network_name_for_project(compose: PodmanCompose, net: str, is_ext: A 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: return f"{compose.project_name.replace('-', '')}_{net}" return f"{compose.project_name}_{net}" @@ -1968,6 +1970,12 @@ COMPOSE_DEFAULT_LS = [ 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: self.podman: Podman self.podman_version: str | None = None @@ -1988,7 +1996,7 @@ class PodmanCompose: self.services: dict[str, Any] self.all_services: set[Any] = set() self.prefer_volume_over_mount = True - self.x_podman: dict[str, Any] = {} + self.x_podman: dict[PodmanCompose.XPodmanSettingKey, Any] = {} self.merged_yaml: Any self.yaml_hash = "" self.console_colors = [ @@ -2059,7 +2067,7 @@ class PodmanCompose: def resolve_in_pod(self) -> bool: 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 return self.global_args.in_pod @@ -2070,7 +2078,43 @@ class PodmanCompose: # - Default value if self.global_args.pod_args is not None: 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: args = self.global_args @@ -2219,6 +2263,8 @@ class PodmanCompose: log.debug(" ** merged:\n%s", json.dumps(compose, indent=2)) # ver = compose.get('version') + self._parse_x_podman_settings(compose, self.environ) + services: dict | None = compose.get("services") if services is None: services = {} @@ -2236,7 +2282,7 @@ class PodmanCompose: nets["default"] = None 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, # docker-compose will create default network named '_default' # and add the service to the default network. @@ -2353,8 +2399,6 @@ class PodmanCompose: given_containers.sort(key=lambda c: len(c.get("_deps", []))) # log("sorted:", [c["name"] for c in given_containers]) - self.x_podman = compose.get("x-podman", {}) - args.in_pod = self.resolve_in_pod() args.pod_arg_list = self.resolve_pod_args() pods, containers = transform(args, project_name, given_containers) diff --git a/tests/integration/in_pod/test_podman_compose_in_pod.py b/tests/integration/in_pod/test_podman_compose_in_pod.py index 099a684..7b60886 100644 --- a/tests/integration/in_pod/test_podman_compose_in_pod.py +++ b/tests/integration/in_pod/test_podman_compose_in_pod.py @@ -467,6 +467,57 @@ class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin): # 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_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: """ Test that podman-compose will create a pod with a custom name diff --git a/tests/integration/test_utils.py b/tests/integration/test_utils.py index d8adedb..828a4f4 100644 --- a/tests/integration/test_utils.py +++ b/tests/integration/test_utils.py @@ -34,7 +34,7 @@ class RunSubprocessMixin: def is_debug_enabled(self) -> bool: 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() if self.is_debug_enabled(): print("TEST_CALL", args) @@ -42,6 +42,7 @@ class RunSubprocessMixin: args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env=os.environ | env, ) out, err = proc.communicate() if self.is_debug_enabled(): @@ -51,9 +52,9 @@ class RunSubprocessMixin: return out, err, proc.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]: - out, err, returncode = self.run_subprocess(args) + out, err, returncode = self.run_subprocess(args, env=env) decoded_out = out.decode('utf-8') decoded_err = err.decode('utf-8') self.assertEqual( # type: ignore[attr-defined] diff --git a/tests/unit/test_container_to_args.py b/tests/unit/test_container_to_args.py index 0825b9b..db4e364 100644 --- a/tests/unit/test_container_to_args.py +++ b/tests/unit/test_container_to_args.py @@ -650,7 +650,7 @@ class TestContainerToArgs(unittest.IsolatedAsyncioTestCase): self, name: str, is_compat: bool, project_name: str, expected_network_name: str ) -> None: 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': {}} cnt = get_minimal_container()