From 8ef537e247697e03fe3da2e082d80c4dcb236ec6 Mon Sep 17 00:00:00 2001 From: Monika Kairaityte Date: Sun, 2 Mar 2025 22:04:17 +0200 Subject: [PATCH 01/10] test/integration: Automate manual `selinux` test Signed-off-by: Monika Kairaityte --- tests/integration/selinux/docker-compose.yml | 18 +++--- tests/integration/selinux/host_test_text.txt | 1 + .../selinux/test_podman_compose_selinux.py | 58 +++++++++++++++++++ 3 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 tests/integration/selinux/host_test_text.txt create mode 100644 tests/integration/selinux/test_podman_compose_selinux.py diff --git a/tests/integration/selinux/docker-compose.yml b/tests/integration/selinux/docker-compose.yml index 24caab6..d166bc8 100644 --- a/tests/integration/selinux/docker-compose.yml +++ b/tests/integration/selinux/docker-compose.yml @@ -1,14 +1,18 @@ version: "3" services: - web1: + container1: image: busybox - command: httpd -f -p 80 -h /var/www/html + command: ["busybox", "sleep", "infinity"] volumes: - type: bind - source: ./docker-compose.yml - target: /var/www/html/index.html + source: ./host_test_text.txt + target: /test_text.txt bind: selinux: z - ports: - - "8080:80" - + container2: + image: busybox + command: ["busybox", "sleep", "infinity"] + volumes: + - type: bind + source: ./host_test_text.txt + target: /test_text.txt diff --git a/tests/integration/selinux/host_test_text.txt b/tests/integration/selinux/host_test_text.txt new file mode 100644 index 0000000..13462fe --- /dev/null +++ b/tests/integration/selinux/host_test_text.txt @@ -0,0 +1 @@ +# There must be a source file in the host for volumes type: bind diff --git a/tests/integration/selinux/test_podman_compose_selinux.py b/tests/integration/selinux/test_podman_compose_selinux.py new file mode 100644 index 0000000..2c04d18 --- /dev/null +++ b/tests/integration/selinux/test_podman_compose_selinux.py @@ -0,0 +1,58 @@ +# SPDX-License-Identifier: GPL-2.0 + +import json +import os +import subprocess +import unittest + +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 TestPodmanCompose(unittest.TestCase, RunSubprocessMixin): + def test_selinux(self): + # test if when using volumes type:bind with selinux:z option, container ackquires a + # respective host:source:z mapping in CreateCommand list + compose_path = os.path.join(test_path(), "selinux", "docker-compose.yml") + try: + # change working directory to where docker-compose.yml file is so that containers can + # directly access host source file for mounting from that working directory + subprocess.run( + [ + podman_compose_path(), + "-f", + compose_path, + "up", + "-d", + "container1", + "container2", + ], + cwd=os.path.join(test_path(), 'selinux'), + ) + out, _ = self.run_subprocess_assert_returncode([ + "podman", + "inspect", + "selinux_container1_1", + ]) + inspect_out = json.loads(out) + create_command_list = inspect_out[0].get("Config", []).get("CreateCommand", {}) + self.assertIn('./host_test_text.txt:/test_text.txt:z', create_command_list) + + out, _ = self.run_subprocess_assert_returncode([ + "podman", + "inspect", + "selinux_container2_1", + ]) + inspect_out = json.loads(out) + create_command_list = inspect_out[0].get("Config", []).get("CreateCommand", {}) + self.assertIn('./host_test_text.txt:/test_text.txt', create_command_list) + finally: + out, _ = self.run_subprocess_assert_returncode([ + podman_compose_path(), + "-f", + compose_path, + "down", + "-t", + "0", + ]) From 2e7d83f7f01526ccb0d2f2d696988ef879c1b547 Mon Sep 17 00:00:00 2001 From: charliemirabile <46761267+charliemirabile@users.noreply.github.com> Date: Mon, 17 Mar 2025 18:42:32 -0400 Subject: [PATCH 02/10] Properly surface errors from build commands the commit 38b13a3 ("Use asyncio for subprocess calls") broke the way exit codes are reported from the podman compose build command. The tasks are awaited as they finish which means that if a later build finishes sucessfully after a failing build, it overwrites status. Previously the `parse_return_code` function would skip updating the status if the new return code was zero, but in removing it, this logic was not carried forward. Fixes: 38b13a3 ("Use asyncio for subprocess calls") Signed-off-by: charliemirabile <46761267+charliemirabile@users.noreply.github.com> --- podman_compose.py | 2 +- .../integration/build_fail_multi/__init__.py | 0 .../build_fail_multi/bad/Dockerfile | 3 ++ .../build_fail_multi/docker-compose.yml | 8 +++++ .../build_fail_multi/good/Dockerfile | 3 ++ .../test_podman_compose_build_fail_multi.py | 31 +++++++++++++++++++ 6 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 tests/integration/build_fail_multi/__init__.py create mode 100644 tests/integration/build_fail_multi/bad/Dockerfile create mode 100644 tests/integration/build_fail_multi/docker-compose.yml create mode 100644 tests/integration/build_fail_multi/good/Dockerfile create mode 100644 tests/integration/build_fail_multi/test_podman_compose_build_fail_multi.py diff --git a/podman_compose.py b/podman_compose.py index df1f9ee..8920065 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -2613,7 +2613,7 @@ async def compose_build(compose, args): status = 0 for t in asyncio.as_completed(tasks): s = await t - if s is not None: + if s is not None and s != 0: status = s return status diff --git a/tests/integration/build_fail_multi/__init__.py b/tests/integration/build_fail_multi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/build_fail_multi/bad/Dockerfile b/tests/integration/build_fail_multi/bad/Dockerfile new file mode 100644 index 0000000..8ee7eb8 --- /dev/null +++ b/tests/integration/build_fail_multi/bad/Dockerfile @@ -0,0 +1,3 @@ +FROM busybox + +RUN false diff --git a/tests/integration/build_fail_multi/docker-compose.yml b/tests/integration/build_fail_multi/docker-compose.yml new file mode 100644 index 0000000..d5de8e9 --- /dev/null +++ b/tests/integration/build_fail_multi/docker-compose.yml @@ -0,0 +1,8 @@ +version: "3" +services: + bad: + build: + context: bad + good: + build: + context: good diff --git a/tests/integration/build_fail_multi/good/Dockerfile b/tests/integration/build_fail_multi/good/Dockerfile new file mode 100644 index 0000000..d1c8850 --- /dev/null +++ b/tests/integration/build_fail_multi/good/Dockerfile @@ -0,0 +1,3 @@ +FROM busybox +#ensure that this build finishes second so that it has a chance to overwrite the return code +RUN sleep 0.5 diff --git a/tests/integration/build_fail_multi/test_podman_compose_build_fail_multi.py b/tests/integration/build_fail_multi/test_podman_compose_build_fail_multi.py new file mode 100644 index 0000000..167becb --- /dev/null +++ b/tests/integration/build_fail_multi/test_podman_compose_build_fail_multi.py @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: GPL-2.0 + +import os +import unittest + +from tests.integration.test_utils import RunSubprocessMixin +from tests.integration.test_utils import podman_compose_path +from tests.integration.test_utils import test_path + + +def compose_yaml_path(): + """ "Returns the path to the compose file used for this test module""" + base_path = os.path.join(test_path(), "build_fail_multi") + return os.path.join(base_path, "docker-compose.yml") + + +class TestComposeBuildFailMulti(unittest.TestCase, RunSubprocessMixin): + def test_build_fail_multi(self): + output, error = self.run_subprocess_assert_returncode( + [ + podman_compose_path(), + "-f", + compose_yaml_path(), + "build", + # prevent the successful build from being cached to ensure it runs long enough + "--no-cache", + ], + expected_returncode=1, + ) + self.assertIn("RUN false", str(output)) + self.assertIn("while running runtime: exit status 1", str(error)) From 3973c476c47cf5f760281b6c9e9bec57aa72c04e Mon Sep 17 00:00:00 2001 From: Mohammad Kazemi Date: Mon, 24 Feb 2025 10:40:42 +0330 Subject: [PATCH 03/10] catch SIGINT signal properly in 'up' function and call compose 'down' function for a graceful shutdown Signed-off-by: Mohammad Kazemi --- podman_compose.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/podman_compose.py b/podman_compose.py index 8920065..32f8dfe 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -2778,9 +2778,17 @@ async def compose_up(compose: PodmanCompose, args): max_service_length = curr_length if curr_length > max_service_length else max_service_length tasks = set() + + async def handle_sigint(): + log.info("Caught SIGINT, shutting down...") + down_args = argparse.Namespace(**dict(args.__dict__, volumes=False)) + await compose.commands["down"](compose, down_args) + for task in tasks: + task.cancel() + if sys.platform != 'win32': loop = asyncio.get_event_loop() - loop.add_signal_handler(signal.SIGINT, lambda: [t.cancel("User exit") for t in tasks]) + loop.add_signal_handler(signal.SIGINT, lambda: asyncio.create_task(handle_sigint())) for i, cnt in enumerate(compose.containers): # Add colored service prefix to output by piping output through sed From b748c2666cc3c92d382f2d57379f5f9c15c1f5fc Mon Sep 17 00:00:00 2001 From: Mohammad Kazemi Date: Mon, 24 Feb 2025 15:18:45 +0330 Subject: [PATCH 04/10] add try-except block to handle error in case of shutdown error Signed-off-by: Mohammad Kazemi --- podman_compose.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/podman_compose.py b/podman_compose.py index 32f8dfe..c7a1f56 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -2780,11 +2780,16 @@ async def compose_up(compose: PodmanCompose, args): tasks = set() async def handle_sigint(): - log.info("Caught SIGINT, shutting down...") - down_args = argparse.Namespace(**dict(args.__dict__, volumes=False)) - await compose.commands["down"](compose, down_args) - for task in tasks: - task.cancel() + log.info("Caught SIGINT or Ctrl+C, shutting down...") + try: + log.info("Shutting down gracefully, please wait...") + down_args = argparse.Namespace(**dict(args.__dict__, volumes=False)) + await compose.commands["down"](compose, down_args) + except Exception as e: + log.error(f"Error during shutdown: {e}") + finally: + for task in tasks: + task.cancel() if sys.platform != 'win32': loop = asyncio.get_event_loop() From f106ea0c012098b341a9b9abec5b3f5fb63b396c Mon Sep 17 00:00:00 2001 From: Mohammad Kazemi Date: Sat, 1 Mar 2025 12:34:58 +0330 Subject: [PATCH 05/10] modifications to pass pylint test Signed-off-by: Mohammad Kazemi --- .pylintrc | 2 +- podman_compose.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pylintrc b/.pylintrc index 75169ce..05cf9e1 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,7 +1,7 @@ [MESSAGES CONTROL] # C0111 missing-docstring: missing-class-docstring, missing-function-docstring, missing-method-docstring, missing-module-docstrin # consider-using-with: we need it for color formatter pipe -disable=too-many-lines,too-many-branches,too-many-locals,too-many-statements,too-many-arguments,too-many-instance-attributes,fixme,multiple-statements,missing-docstring,line-too-long,consider-using-f-string,consider-using-with,unnecessary-lambda-assignment +disable=too-many-lines,too-many-branches,too-many-locals,too-many-statements,too-many-arguments,too-many-instance-attributes,fixme,multiple-statements,missing-docstring,line-too-long,consider-using-f-string,consider-using-with,unnecessary-lambda-assignment,broad-exception-caught # allow _ for ignored variables # allow generic names like a,b,c and i,j,k,l,m,n and x,y,z # allow k,v for key/value diff --git a/podman_compose.py b/podman_compose.py index c7a1f56..844027a 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -2786,7 +2786,7 @@ async def compose_up(compose: PodmanCompose, args): down_args = argparse.Namespace(**dict(args.__dict__, volumes=False)) await compose.commands["down"](compose, down_args) except Exception as e: - log.error(f"Error during shutdown: {e}") + log.error("Error during shutdown: %s", e) finally: for task in tasks: task.cancel() From f5a6df6dc4cea912c9de9e070ffe523085035752 Mon Sep 17 00:00:00 2001 From: Mohammad Kazemi Date: Wed, 19 Mar 2025 16:02:34 +0330 Subject: [PATCH 06/10] added changes to release notes Signed-off-by: Mohammad Kazemi --- newsfragments/sigint-up.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/sigint-up.bugfix diff --git a/newsfragments/sigint-up.bugfix b/newsfragments/sigint-up.bugfix new file mode 100644 index 0000000..8edd22c --- /dev/null +++ b/newsfragments/sigint-up.bugfix @@ -0,0 +1 @@ +- Fixed handling SIGINT when running "up" command to shutdown gracefully \ No newline at end of file From 170411de8be48960679361c6ab8e6b7256902904 Mon Sep 17 00:00:00 2001 From: Ruben Jenster Date: Wed, 12 Mar 2025 16:20:58 +0100 Subject: [PATCH 07/10] test/integration: Test COMPOSE_PROJECT_NAME interpolation Refs #1073 Signed-off-by: Ruben Jenster --- .../env-tests/container-compose.yml | 8 +++++ .../env-tests/test_podman_compose_env.py | 31 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/tests/integration/env-tests/container-compose.yml b/tests/integration/env-tests/container-compose.yml index 3a8b28e..4498f09 100644 --- a/tests/integration/env-tests/container-compose.yml +++ b/tests/integration/env-tests/container-compose.yml @@ -1,5 +1,7 @@ version: "3" +name: my-project-name + services: env-test: image: busybox @@ -8,3 +10,9 @@ services: ZZVAR1: myval1 ZZVAR2: 2-$ZZVAR1 ZZVAR3: 3-$ZZVAR2 + + project-name-test: + image: busybox + command: sh -c "echo $$PNAME" + environment: + PNAME: ${COMPOSE_PROJECT_NAME} diff --git a/tests/integration/env-tests/test_podman_compose_env.py b/tests/integration/env-tests/test_podman_compose_env.py index 351644f..3d50a07 100644 --- a/tests/integration/env-tests/test_podman_compose_env.py +++ b/tests/integration/env-tests/test_podman_compose_env.py @@ -36,3 +36,34 @@ class TestComposeEnv(unittest.TestCase, RunSubprocessMixin): compose_yaml_path(), "down", ]) + + """ + Tests interpolation of COMPOSE_PROJECT_NAME in the podman-compose config, + which is different from external environment variables because COMPOSE_PROJECT_NAME + is a predefined environment variable generated from the `name` value in the top-level + of the compose.yaml. + + See also + - https://docs.docker.com/reference/compose-file/interpolation/ + - https://docs.docker.com/reference/compose-file/version-and-name/#name-top-level-element + - https://docs.docker.com/compose/how-tos/environment-variables/envvars/ + - https://github.com/compose-spec/compose-spec/blob/main/04-version-and-name.md + """ + + def test_project_name(self): + try: + output, _ = self.run_subprocess_assert_returncode([ + podman_compose_path(), + "-f", + compose_yaml_path(), + "run", + "project-name-test", + ]) + self.assertIn("my-project-name", str(output)) + finally: + self.run_subprocess_assert_returncode([ + podman_compose_path(), + "-f", + compose_yaml_path(), + "down", + ]) From 98b9bb9f8ed18315f87bd5106ae29ca70b8866c6 Mon Sep 17 00:00:00 2001 From: Ruben Jenster Date: Thu, 20 Mar 2025 12:06:19 +0100 Subject: [PATCH 08/10] Fix interpolation for COMPOSE_PROJECT_NAME Fixes #1073 Signed-off-by: Ruben Jenster --- .../1165-PROJECT_NAME-interpolation.bugfix | 1 + podman_compose.py | 27 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) create mode 100644 newsfragments/1165-PROJECT_NAME-interpolation.bugfix diff --git a/newsfragments/1165-PROJECT_NAME-interpolation.bugfix b/newsfragments/1165-PROJECT_NAME-interpolation.bugfix new file mode 100644 index 0000000..e4200e0 --- /dev/null +++ b/newsfragments/1165-PROJECT_NAME-interpolation.bugfix @@ -0,0 +1 @@ +- Fixed interpolation of the environment variable **COMPOSE_PROJECT_NAME** when it is set from the top-level **name** value within the Compose file. diff --git a/podman_compose.py b/podman_compose.py index df1f9ee..379d9dc 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -1980,6 +1980,20 @@ class PodmanCompose: sys.exit(1) content = normalize(content) # log(filename, json.dumps(content, indent = 2)) + + if not project_name: + project_name = content.get("name") + if project_name is None: + # More strict then actually needed for simplicity: + # podman requires [a-zA-Z0-9][a-zA-Z0-9_.-]* + project_name = self.environ.get("COMPOSE_PROJECT_NAME", dir_basename.lower()) + project_name = norm_re.sub("", project_name) + if not project_name: + raise RuntimeError(f"Project name [{dir_basename}] normalized to empty") + + self.project_name = project_name + self.environ.update({"COMPOSE_PROJECT_NAME": self.project_name}) + content = rec_subs(content, self.environ) if isinstance(services := content.get('services'), dict): for service in services.values(): @@ -2012,19 +2026,6 @@ class PodmanCompose: log.debug(" ** merged:\n%s", json.dumps(compose, indent=2)) # ver = compose.get('version') - if not project_name: - project_name = compose.get("name") - if project_name is None: - # More strict then actually needed for simplicity: - # podman requires [a-zA-Z0-9][a-zA-Z0-9_.-]* - project_name = self.environ.get("COMPOSE_PROJECT_NAME", dir_basename.lower()) - project_name = norm_re.sub("", project_name) - if not project_name: - raise RuntimeError(f"Project name [{dir_basename}] normalized to empty") - - self.project_name = project_name - self.environ.update({"COMPOSE_PROJECT_NAME": self.project_name}) - services = compose.get("services") if services is None: services = {} From 1aa750bacfa0fa629642713f956b144d3d41ba21 Mon Sep 17 00:00:00 2001 From: Ruben Jenster Date: Wed, 12 Mar 2025 19:10:05 +0100 Subject: [PATCH 09/10] integration/tests: Test project name override with COMPOSE_PROJECT_NAME env variable Signed-off-by: Ruben Jenster --- .../env-tests/test_podman_compose_env.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/integration/env-tests/test_podman_compose_env.py b/tests/integration/env-tests/test_podman_compose_env.py index 3d50a07..f1c35cf 100644 --- a/tests/integration/env-tests/test_podman_compose_env.py +++ b/tests/integration/env-tests/test_podman_compose_env.py @@ -67,3 +67,23 @@ class TestComposeEnv(unittest.TestCase, RunSubprocessMixin): compose_yaml_path(), "down", ]) + + def test_project_name_override(self): + try: + output, _ = self.run_subprocess_assert_returncode([ + podman_compose_path(), + "-f", + compose_yaml_path(), + "run", + "-e", + "COMPOSE_PROJECT_NAME=project-name-override", + "project-name-test", + ]) + self.assertIn("project-name-override", str(output)) + finally: + self.run_subprocess_assert_returncode([ + podman_compose_path(), + "-f", + compose_yaml_path(), + "down", + ]) From 65b455f0811189859baedb4f69d36a55b388274f Mon Sep 17 00:00:00 2001 From: Ruben Jenster Date: Wed, 12 Mar 2025 19:04:38 +0100 Subject: [PATCH 10/10] Fix project name evaluation order The COMPOSE_PROJECT_NAME environment variable must override the top-level name: attribute in the Compose file. The precedence order is defined in the docker compose documentation https://docs.docker.com/compose/how-tos/project-name/#set-a-project-name Signed-off-by: Ruben Jenster --- .../1165-project-name-evaluation-order.bugfix | 1 + podman_compose.py | 21 ++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 newsfragments/1165-project-name-evaluation-order.bugfix diff --git a/newsfragments/1165-project-name-evaluation-order.bugfix b/newsfragments/1165-project-name-evaluation-order.bugfix new file mode 100644 index 0000000..d8834e1 --- /dev/null +++ b/newsfragments/1165-project-name-evaluation-order.bugfix @@ -0,0 +1 @@ +- Fixed project name evaluation order to match the order defined in the [compose spec](https://docs.docker.com/compose/how-tos/project-name/#set-a-project-name). diff --git a/podman_compose.py b/podman_compose.py index 379d9dc..62ec408 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -1981,15 +1981,20 @@ class PodmanCompose: content = normalize(content) # log(filename, json.dumps(content, indent = 2)) + # See also https://docs.docker.com/compose/how-tos/project-name/#set-a-project-name + # **project_name** is initialized to the argument of the `-p` command line flag. if not project_name: - project_name = content.get("name") - if project_name is None: - # More strict then actually needed for simplicity: - # podman requires [a-zA-Z0-9][a-zA-Z0-9_.-]* - project_name = self.environ.get("COMPOSE_PROJECT_NAME", dir_basename.lower()) - project_name = norm_re.sub("", project_name) - if not project_name: - raise RuntimeError(f"Project name [{dir_basename}] normalized to empty") + project_name = self.environ.get("COMPOSE_PROJECT_NAME") + if not project_name: + project_name = content.get("name") + if not project_name: + project_name = dir_basename.lower() + # More strict then actually needed for simplicity: + # podman requires [a-zA-Z0-9][a-zA-Z0-9_.-]* + project_name_normalized = norm_re.sub("", project_name) + if not project_name_normalized: + raise RuntimeError(f"Project name [{project_name}] normalized to empty") + project_name = project_name_normalized self.project_name = project_name self.environ.update({"COMPOSE_PROJECT_NAME": self.project_name})