From d7762a54f08f8c19624334722394eb9363befb4a Mon Sep 17 00:00:00 2001 From: Justin Zhang Date: Sat, 10 May 2025 14:07:49 +0300 Subject: [PATCH] Fix service_healthy condition enforcing Skip dependency health check to avoid compose-up hang for podman prior to 4.6.0, which doesn't support --condition healthy. Signed-off-by: Justin Zhang --- newsfragments/1178.bugfix | 1 + newsfragments/1183.bugfix | 1 + podman_compose.py | 12 +++ .../docker-compose-conditional-healthy.yaml | 23 ++++++ .../deps/test_podman_compose_deps.py | 81 +++++++++++++++++++ tests/integration/test_utils.py | 21 +++++ 6 files changed, 139 insertions(+) create mode 100644 newsfragments/1178.bugfix create mode 100644 newsfragments/1183.bugfix create mode 100644 tests/integration/deps/docker-compose-conditional-healthy.yaml diff --git a/newsfragments/1178.bugfix b/newsfragments/1178.bugfix new file mode 100644 index 0000000..ac6f543 --- /dev/null +++ b/newsfragments/1178.bugfix @@ -0,0 +1 @@ +Fixed up command hangs on Podman versions earlier than 4.6.0 (#1178) diff --git a/newsfragments/1183.bugfix b/newsfragments/1183.bugfix new file mode 100644 index 0000000..023002c --- /dev/null +++ b/newsfragments/1183.bugfix @@ -0,0 +1 @@ +- Fixed issue in up command where service_healthy conditions weren't being enforced (#1183) diff --git a/podman_compose.py b/podman_compose.py index 22d55cd..7cc4ef2 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -2815,6 +2815,18 @@ async def check_dep_conditions(compose: PodmanCompose, deps: set) -> None: deps_cd = [] for d in deps: if d.condition == condition: + if ( + d.condition + in (ServiceDependencyCondition.HEALTHY, ServiceDependencyCondition.UNHEALTHY) + ) and strverscmp_lt(compose.podman_version, "4.6.0"): + log.warning( + "Ignored %s condition check due to podman %s doesn't support %s!", + d.name, + compose.podman_version, + condition.value, + ) + continue + deps_cd.extend(compose.container_names_by_service[d.name]) if deps_cd: diff --git a/tests/integration/deps/docker-compose-conditional-healthy.yaml b/tests/integration/deps/docker-compose-conditional-healthy.yaml new file mode 100644 index 0000000..59b0d03 --- /dev/null +++ b/tests/integration/deps/docker-compose-conditional-healthy.yaml @@ -0,0 +1,23 @@ +version: "3.7" +services: + web: + image: nopush/podman-compose-test + command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-h", "/etc/", "-p", "8000"] + tmpfs: + - /run + - /tmp + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:8000/hosts"] + start_period: 10s # initialization time for containers that need time to bootstrap + interval: 10s # Time between health checks + timeout: 5s # Time to wait for a response + retries: 3 # Number of consecutive failures before marking as unhealthy + sleep: + image: nopush/podman-compose-test + command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 3600"] + depends_on: + web: + condition: service_healthy + tmpfs: + - /run + - /tmp diff --git a/tests/integration/deps/test_podman_compose_deps.py b/tests/integration/deps/test_podman_compose_deps.py index e15f6ec..1569a1c 100644 --- a/tests/integration/deps/test_podman_compose_deps.py +++ b/tests/integration/deps/test_podman_compose_deps.py @@ -2,7 +2,9 @@ import os import unittest +from tests.integration.test_utils import PodmanAwareRunSubprocessMixin from tests.integration.test_utils import RunSubprocessMixin +from tests.integration.test_utils import is_systemd_available from tests.integration.test_utils import podman_compose_path from tests.integration.test_utils import test_path @@ -183,3 +185,82 @@ class TestComposeConditionalDeps(unittest.TestCase, RunSubprocessMixin): compose_yaml_path(suffix), "down", ]) + + +class TestComposeConditionalDepsHealthy(unittest.TestCase, PodmanAwareRunSubprocessMixin): + def setUp(self): + self.podman_version = self.retrieve_podman_version() + + def test_up_deps_healthy(self): + suffix = "-conditional-healthy" + try: + self.run_subprocess_assert_returncode([ + podman_compose_path(), + "-f", + compose_yaml_path(suffix), + "up", + "sleep", + "--detach", + ]) + + # Since the command `podman wait --condition=healthy` is invalid prior to 4.6.0, + # we only validate healthy status for podman 4.6.0+, which won't be tested in the + # CI pipeline of the podman-compose project where podman 4.3.1 is employed. + podman_ver_major, podman_ver_minor, podman_ver_patch = self.podman_version + if podman_ver_major >= 4 and podman_ver_minor >= 6 and podman_ver_patch >= 0: + self.run_subprocess_assert_returncode([ + "podman", + "wait", + "--condition=running", + "deps_web_1", + "deps_sleep_1", + ]) + + # check both web and sleep are running + output, _ = self.run_subprocess_assert_returncode([ + podman_compose_path(), + "-f", + compose_yaml_path(), + "ps", + "--format", + "{{.ID}}\t{{.Names}}\t{{.Status}}\t{{.StartedAt}}", + ]) + + # extract container id of web + decoded_out = output.decode('utf-8') + lines = decoded_out.split("\n") + + web_lines = [line for line in lines if "web" in line] + self.assertTrue(web_lines) + self.assertEqual(1, len(web_lines)) + web_cnt_id, web_cnt_name, web_cnt_status, web_cnt_started = web_lines[0].split("\t") + self.assertNotEqual("", web_cnt_id) + self.assertEqual("deps_web_1", web_cnt_name) + + sleep_lines = [line for line in lines if "sleep" in line] + self.assertTrue(sleep_lines) + self.assertEqual(1, len(sleep_lines)) + sleep_cnt_id, sleep_cnt_name, _, sleep_cnt_started = sleep_lines[0].split("\t") + self.assertNotEqual("", sleep_cnt_id) + self.assertEqual("deps_sleep_1", sleep_cnt_name) + + # When test case is executed inside container like github actions, the absence of + # systemd prevents health check from working properly, resulting in failure to + # transit to healthy state. As a result, we only assert the `healthy` state where + # systemd is functioning. + if ( + is_systemd_available() + and podman_ver_major >= 4 + and podman_ver_minor >= 6 + and podman_ver_patch >= 0 + ): + self.assertIn("healthy", web_cnt_status) + self.assertGreaterEqual(int(sleep_cnt_started), int(web_cnt_started)) + + finally: + self.run_subprocess_assert_returncode([ + podman_compose_path(), + "-f", + compose_yaml_path(), + "down", + ]) diff --git a/tests/integration/test_utils.py b/tests/integration/test_utils.py index 475762d..5d170ba 100644 --- a/tests/integration/test_utils.py +++ b/tests/integration/test_utils.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: GPL-2.0 import os +import re import subprocess import time from pathlib import Path @@ -21,6 +22,14 @@ def podman_compose_path(): return os.path.join(base_path(), "podman_compose.py") +def is_systemd_available(): + try: + with open("/proc/1/comm", "r", encoding="utf-8") as fh: + return fh.read().strip() == "systemd" + except FileNotFoundError: + return False + + class RunSubprocessMixin: def is_debug_enabled(self): return "TESTS_DEBUG" in os.environ @@ -52,3 +61,15 @@ class RunSubprocessMixin: f"stdout: {decoded_out}\nstderr: {decoded_err}\n", ) return out, err + + +class PodmanAwareRunSubprocessMixin(RunSubprocessMixin): + def retrieve_podman_version(self): + out, _ = self.run_subprocess_assert_returncode(["podman", "--version"]) + matcher = re.match(r"\D*(\d+)\.(\d+)\.(\d+)", out.decode('utf-8')) + if matcher: + major = int(matcher.group(1)) + minor = int(matcher.group(2)) + patch = int(matcher.group(3)) + return (major, minor, patch) + raise RuntimeError("Unable to retrieve podman version")