diff --git a/docs/Extensions.md b/docs/Extensions.md index ae1c011..3244f5f 100644 --- a/docs/Extensions.md +++ b/docs/Extensions.md @@ -89,3 +89,23 @@ In addition, podman-compose supports the following podman-specific values for `n The options to the network modes are passed to the `--network` option of the `podman create` command as-is. + + +## Custom pods management + +Podman-compose can have containers in pods. This can be controlled by extension key x-podman in_pod. +It allows providing custom value for --in-pod and is especially relevant when --userns has to be set. + +For example, the following docker-compose.yml allows using userns_mode by overriding the default +value of --in-pod (unless it was specifically provided by "--in-pod=True" in command line interface). +```yml +version: "3" +services: + cont: + image: nopush/podman-compose-test + userns_mode: keep-id:uid=1000 + command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-p", "8080"] + +x-podman: + in_pod: false +``` diff --git a/podman_compose.py b/podman_compose.py index 39ed03c..be3318f 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -1741,6 +1741,18 @@ class PodmanCompose: if isinstance(retcode, int): sys.exit(retcode) + def resolve_in_pod(self, compose): + if self.global_args.in_pod_bool is None: + extension_dict = compose.get("x-podman", None) + if extension_dict is not None: + in_pod_value = extension_dict.get("in_pod", None) + if in_pod_value is not None: + self.global_args.in_pod_bool = in_pod_value + else: + self.global_args.in_pod_bool = True + # otherwise use `in_pod` value provided by command line + return self.global_args.in_pod_bool + def _parse_compose_file(self): args = self.global_args # cmd = args.command @@ -1968,6 +1980,8 @@ class PodmanCompose: given_containers = list(container_by_name.values()) given_containers.sort(key=lambda c: len(c.get("_deps", None) or [])) # log("sorted:", [c["name"] for c in given_containers]) + + args.in_pod_bool = self.resolve_in_pod(compose) pods, containers = transform(args, project_name, given_containers) self.pods = pods self.containers = containers @@ -2005,12 +2019,22 @@ class PodmanCompose: for cmd_parser in cmd._parse_args: # pylint: disable=protected-access cmd_parser(subparser) self.global_args = parser.parse_args() - if self.global_args.in_pod.lower() not in ('', 'true', '1', 'false', '0'): + if self.global_args.in_pod is not None and self.global_args.in_pod.lower() not in ( + '', + 'true', + '1', + 'false', + '0', + ): raise ValueError( f'Invalid --in-pod value: \'{self.global_args.in_pod}\'. ' 'It must be set to either of: empty value, true, 1, false, 0' ) - self.global_args.in_pod_bool = self.global_args.in_pod.lower() in ('', 'true', '1') + + if self.global_args.in_pod == '' or self.global_args.in_pod is None: + self.global_args.in_pod_bool = None + else: + self.global_args.in_pod_bool = self.global_args.in_pod.lower() in ('true', '1') if self.global_args.version: self.global_args.command = "version" @@ -2029,7 +2053,7 @@ class PodmanCompose: help="pod creation", metavar="in_pod", type=str, - default="true", + default=None, ) parser.add_argument( "--pod-args", diff --git a/tests/in_pod/custom_x-podman_false/docker-compose.yml b/tests/in_pod/custom_x-podman_false/docker-compose.yml new file mode 100644 index 0000000..c967bef --- /dev/null +++ b/tests/in_pod/custom_x-podman_false/docker-compose.yml @@ -0,0 +1,9 @@ +version: "3" +services: + cont: + image: nopush/podman-compose-test + userns_mode: keep-id:uid=1000 + command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-p", "8080"] + +x-podman: + in_pod: false diff --git a/tests/in_pod/custom_x-podman_not_exists/docker-compose.yml b/tests/in_pod/custom_x-podman_not_exists/docker-compose.yml new file mode 100644 index 0000000..8514c79 --- /dev/null +++ b/tests/in_pod/custom_x-podman_not_exists/docker-compose.yml @@ -0,0 +1,6 @@ +version: "3" +services: + cont: + image: nopush/podman-compose-test + userns_mode: keep-id:uid=1000 + command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-p", "8080"] diff --git a/tests/in_pod/custom_x-podman_true/docker-compose.yml b/tests/in_pod/custom_x-podman_true/docker-compose.yml new file mode 100644 index 0000000..698f7b4 --- /dev/null +++ b/tests/in_pod/custom_x-podman_true/docker-compose.yml @@ -0,0 +1,9 @@ +version: "3" +services: + cont: + image: nopush/podman-compose-test + userns_mode: keep-id:uid=1000 + command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-p", "8080"] + +x-podman: + in_pod: true diff --git a/tests/test_podman_compose_in_pod.py b/tests/test_podman_compose_in_pod.py new file mode 100644 index 0000000..ed66a17 --- /dev/null +++ b/tests/test_podman_compose_in_pod.py @@ -0,0 +1,442 @@ +# SPDX-License-Identifier: GPL-2.0 + +import os +import unittest +from pathlib import Path + +from .test_utils import RunSubprocessMixin + + +def base_path(): + """Returns the base path for the project""" + return Path(__file__).parent.parent + + +def test_path(): + """Returns the path to the tests directory""" + return os.path.join(base_path(), "tests") + + +def podman_compose_path(): + """Returns the path to the podman compose script""" + return os.path.join(base_path(), "podman_compose.py") + + +# If a compose file has userns_mode set, setting in_pod to True, results in error. +# Default in_pod setting is True, unless compose file provides otherwise. +# Compose file provides custom in_pod option, which can be overridden by command line in_pod option. +# Test all combinations of command line argument in_pod and compose file argument in_pod. +class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin): + # compose file provides x-podman in_pod=false + def test_x_podman_in_pod_false_command_line_in_pod_not_exists(self): + """ + Test that podman-compose will not create a pod, when x-podman in_pod=false and command line + does not provide this option + """ + main_path = Path(__file__).parent.parent + + command_up = [ + "python3", + str(main_path.joinpath("podman_compose.py")), + "-f", + str( + main_path.joinpath("tests", "in_pod", "custom_x-podman_false", "docker-compose.yml") + ), + "up", + "-d", + ] + + down_cmd = [ + "python3", + podman_compose_path(), + "-f", + str( + main_path.joinpath("tests", "in_pod", "custom_x-podman_false", "docker-compose.yml") + ), + "down", + ] + + try: + self.run_subprocess_assert_returncode(command_up) + + finally: + self.run_subprocess_assert_returncode(down_cmd) + command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_false"] + # throws an error, can not actually find this pod because it was not created + self.run_subprocess_assert_returncode(command_rm_pod, expected_returncode=1) + + def test_x_podman_in_pod_false_command_line_in_pod_true(self): + """ + Test that podman-compose does not allow pod creating even with command line in_pod=True + when --userns and --pod are set together: throws an error + """ + main_path = Path(__file__).parent.parent + + # FIXME: creates a pod anyway, although it should not + command_up = [ + "python3", + str(main_path.joinpath("podman_compose.py")), + "--in-pod=True", + "-f", + str( + main_path.joinpath("tests", "in_pod", "custom_x-podman_false", "docker-compose.yml") + ), + "up", + "-d", + ] + + try: + out, err = self.run_subprocess_assert_returncode(command_up) + self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True) + + finally: + command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_false"] + # should throw an error of not being able to find this pod (because it should not have + # been created) and have expected_returncode=1 (see FIXME above) + self.run_subprocess_assert_returncode(command_rm_pod) + + def test_x_podman_in_pod_false_command_line_in_pod_false(self): + """ + Test that podman-compose will not create a pod as command line sets in_pod=False + """ + main_path = Path(__file__).parent.parent + + command_up = [ + "python3", + str(main_path.joinpath("podman_compose.py")), + "--in-pod=False", + "-f", + str( + main_path.joinpath("tests", "in_pod", "custom_x-podman_false", "docker-compose.yml") + ), + "up", + "-d", + ] + + down_cmd = [ + "python3", + podman_compose_path(), + "-f", + str( + main_path.joinpath("tests", "in_pod", "custom_x-podman_false", "docker-compose.yml") + ), + "down", + ] + + try: + self.run_subprocess_assert_returncode(command_up) + + finally: + self.run_subprocess_assert_returncode(down_cmd) + command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_false"] + # 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_false_command_line_in_pod_empty_string(self): + """ + Test that podman-compose will not create a pod, when x-podman in_pod=false and command line + command line in_pod="" + """ + main_path = Path(__file__).parent.parent + + command_up = [ + "python3", + str(main_path.joinpath("podman_compose.py")), + "--in-pod=", + "-f", + str( + main_path.joinpath("tests", "in_pod", "custom_x-podman_false", "docker-compose.yml") + ), + "up", + "-d", + ] + + down_cmd = [ + "python3", + podman_compose_path(), + "-f", + str( + main_path.joinpath("tests", "in_pod", "custom_x-podman_false", "docker-compose.yml") + ), + "down", + ] + + try: + self.run_subprocess_assert_returncode(command_up) + + finally: + self.run_subprocess_assert_returncode(down_cmd) + command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_false"] + # can not actually find this pod because it was not created + self.run_subprocess_assert_returncode(command_rm_pod, 1) + + # compose file provides x-podman in_pod=true + def test_x_podman_in_pod_true_command_line_in_pod_not_exists(self): + """ + Test that podman-compose does not allow pod creating when --userns and --pod are set + together even when x-podman in_pod=true: throws an error + """ + main_path = Path(__file__).parent.parent + + # FIXME: creates a pod anyway, although it should not + # Container is not created, so command 'down' is not needed + command_up = [ + "python3", + str(main_path.joinpath("podman_compose.py")), + "-f", + str( + main_path.joinpath("tests", "in_pod", "custom_x-podman_true", "docker-compose.yml") + ), + "up", + "-d", + ] + + try: + out, err = self.run_subprocess_assert_returncode(command_up) + self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True) + + finally: + command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_true"] + # should throw an error of not being able to find this pod (it should not have been + # created) and have expected_returncode=1 (see FIXME above) + self.run_subprocess_assert_returncode(command_rm_pod) + + def test_x_podman_in_pod_true_command_line_in_pod_true(self): + """ + Test that podman-compose does not allow pod creating when --userns and --pod are set + together even when x-podman in_pod=true and and command line in_pod=True: throws an error + """ + main_path = Path(__file__).parent.parent + + # FIXME: creates a pod anyway, although it should not + # Container is not created, so command 'down' is not needed + command_up = [ + "python3", + str(main_path.joinpath("podman_compose.py")), + "--in-pod=True", + "-f", + str( + main_path.joinpath("tests", "in_pod", "custom_x-podman_true", "docker-compose.yml") + ), + "up", + "-d", + ] + + try: + out, err = self.run_subprocess_assert_returncode(command_up) + self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True) + + finally: + command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_true"] + # should throw an error of not being able to find this pod (because it should not have + # been created) and have expected_returncode=1 (see FIXME above) + self.run_subprocess_assert_returncode(command_rm_pod) + + def test_x_podman_in_pod_true_command_line_in_pod_false(self): + """ + Test that podman-compose will not create a pod as command line sets in_pod=False + """ + main_path = Path(__file__).parent.parent + + command_up = [ + "python3", + str(main_path.joinpath("podman_compose.py")), + "--in-pod=False", + "-f", + str( + main_path.joinpath("tests", "in_pod", "custom_x-podman_true", "docker-compose.yml") + ), + "up", + "-d", + ] + + down_cmd = [ + "python3", + podman_compose_path(), + "-f", + str( + main_path.joinpath("tests", "in_pod", "custom_x-podman_true", "docker-compose.yml") + ), + "down", + ] + + try: + self.run_subprocess_assert_returncode(command_up) + + finally: + self.run_subprocess_assert_returncode(down_cmd) + command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_false"] + # 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_true_command_line_in_pod_empty_string(self): + """ + Test that podman-compose does not allow pod creating when --userns and --pod are set + together even when x-podman in_pod=true and command line in_pod="": throws an error + """ + main_path = Path(__file__).parent.parent + + # FIXME: creates a pod anyway, although it should not + # Container is not created, so command 'down' is not needed + command_up = [ + "python3", + str(main_path.joinpath("podman_compose.py")), + "--in-pod=", + "-f", + str( + main_path.joinpath("tests", "in_pod", "custom_x-podman_true", "docker-compose.yml") + ), + "up", + "-d", + ] + + try: + out, err = self.run_subprocess_assert_returncode(command_up) + self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True) + + finally: + command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_true"] + # should throw an error of not being able to find this pod (because it should not have + # been created) and have expected_returncode=1 (see FIXME above) + self.run_subprocess_assert_returncode(command_rm_pod) + + # compose file does not provide x-podman in_pod + def test_x_podman_in_pod_not_exists_command_line_in_pod_not_exists(self): + """ + Test that podman-compose does not allow pod creating when --userns and --pod are set + together: throws an error + """ + main_path = Path(__file__).parent.parent + + # FIXME: creates a pod anyway, although it should not + # Container is not created, so command 'down' is not needed + command_up = [ + "python3", + str(main_path.joinpath("podman_compose.py")), + "-f", + str( + main_path.joinpath( + "tests", "in_pod", "custom_x-podman_not_exists", "docker-compose.yml" + ) + ), + "up", + "-d", + ] + + try: + out, err = self.run_subprocess_assert_returncode(command_up) + self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True) + + finally: + command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_not_exists"] + # should throw an error of not being able to find this pod (it should not have been + # created) and have expected_returncode=1 (see FIXME above) + self.run_subprocess_assert_returncode(command_rm_pod) + + def test_x_podman_in_pod_not_exists_command_line_in_pod_true(self): + """ + Test that podman-compose does not allow pod creating when --userns and --pod are set + together even when x-podman in_pod=true: throws an error + """ + main_path = Path(__file__).parent.parent + + # FIXME: creates a pod anyway, although it should not + # Container was not created, so command 'down' is not needed + command_up = [ + "python3", + str(main_path.joinpath("podman_compose.py")), + "--in-pod=True", + "-f", + str( + main_path.joinpath( + "tests", "in_pod", "custom_x-podman_not_exists", "docker-compose.yml" + ) + ), + "up", + "-d", + ] + + try: + out, err = self.run_subprocess_assert_returncode(command_up) + self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True) + + finally: + command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_not_exists"] + # should throw an error of not being able to find this pod (because it should not have + # been created) and have expected_returncode=1 (see FIXME above) + self.run_subprocess_assert_returncode(command_rm_pod) + + def test_x_podman_in_pod_not_exists_command_line_in_pod_false(self): + """ + Test that podman-compose will not create a pod as command line sets in_pod=False + """ + main_path = Path(__file__).parent.parent + + command_up = [ + "python3", + str(main_path.joinpath("podman_compose.py")), + "--in-pod=False", + "-f", + str( + main_path.joinpath( + "tests", "in_pod", "custom_x-podman_not_exists", "docker-compose.yml" + ) + ), + "up", + "-d", + ] + + down_cmd = [ + "python3", + podman_compose_path(), + "-f", + str( + main_path.joinpath( + "tests", "in_pod", "custom_x-podman_not_exists", "docker-compose.yml" + ) + ), + "down", + ] + + try: + self.run_subprocess_assert_returncode(command_up) + + finally: + self.run_subprocess_assert_returncode(down_cmd) + + 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_not_exists_command_line_in_pod_empty_string(self): + """ + Test that podman-compose does not allow pod creating when --userns and --pod are set + together: throws an error + """ + main_path = Path(__file__).parent.parent + + # FIXME: creates a pod anyway, although it should not + # Container was not created, so command 'down' is not needed + command_up = [ + "python3", + str(main_path.joinpath("podman_compose.py")), + "--in-pod=", + "-f", + str( + main_path.joinpath( + "tests", "in_pod", "custom_x-podman_not_exists", "docker-compose.yml" + ) + ), + "up", + "-d", + ] + + try: + out, err = self.run_subprocess_assert_returncode(command_up) + self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True) + + finally: + command_rm_pod = ["podman", "pod", "rm", "pod_custom_x-podman_not_exists"] + # should throw an error of not being able to find this pod (because it should not have + # been created) and have expected_returncode=1 (see FIXME above) + self.run_subprocess_assert_returncode(command_rm_pod)