Skip to content

Commit ac97fdf

Browse files
committed
Fix #1183 to enforce service_healthy condition
- Change compose-up to create then start container to enforce dependency condition check - Skip running compose-down when there are no active containers - Skip dependency health check to avoid compose-up hang for podman prior to 4.6.0, which doesn't support --condition healthy - Add relevant integration test case - Relax the pylint rules for test code Signed-off-by: Justin Zhang <[email protected]>
1 parent 2e46ff0 commit ac97fdf

11 files changed

+196
-34
lines changed

.pre-commit-config.yaml

+14
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,20 @@ repos:
2323
"-sn", # Don't display the score
2424
"--rcfile=.pylintrc", # Link to your config file
2525
]
26+
files: ^[^/]+\.py$
27+
# Hook for test code with different config
28+
- id: pylint
29+
name: pylint-tests
30+
entry: pylint
31+
language: system
32+
types: [python]
33+
args:
34+
[
35+
"-rn", # Only display messages
36+
"-sn", # Don't display the score
37+
"--rcfile=tests/.pylintrc" # Relax the lint rules for test code
38+
]
39+
files: ^tests/
2640
- repo: https://github.com/codespell-project/codespell
2741
rev: v2.2.5
2842
hooks:

podman_compose.py

+17-8
Original file line numberDiff line numberDiff line change
@@ -2708,6 +2708,18 @@ async def check_dep_conditions(compose: PodmanCompose, deps: set) -> None:
27082708
deps_cd = []
27092709
for d in deps:
27102710
if d.condition == condition:
2711+
if (
2712+
d.condition
2713+
in (ServiceDependencyCondition.HEALTHY, ServiceDependencyCondition.UNHEALTHY)
2714+
) and strverscmp_lt(compose.podman_version, "4.6.0"):
2715+
log.warning(
2716+
"Ignored %s condition check due to podman %s doesn't support %s!",
2717+
d.name,
2718+
compose.podman_version,
2719+
condition.value,
2720+
)
2721+
continue
2722+
27112723
deps_cd.extend(compose.container_names_by_service[d.name])
27122724

27132725
if deps_cd:
@@ -2784,28 +2796,25 @@ async def compose_up(compose: PodmanCompose, args):
27842796
.splitlines()
27852797
)
27862798
diff_hashes = [i for i in hashes if i and i != compose.yaml_hash]
2787-
if args.force_recreate or len(diff_hashes):
2799+
if (args.force_recreate and len(hashes) > 0) or len(diff_hashes):
27882800
log.info("recreating: ...")
27892801
down_args = argparse.Namespace(**dict(args.__dict__, volumes=False))
27902802
await compose.commands["down"](compose, down_args)
27912803
log.info("recreating: done\n\n")
27922804
# args.no_recreate disables check for changes (which is not implemented)
27932805

2794-
podman_command = "run" if args.detach and not args.no_start else "create"
2795-
27962806
await create_pods(compose, args)
27972807
for cnt in compose.containers:
27982808
if cnt["_service"] in excluded:
27992809
log.debug("** skipping: %s", cnt["name"])
28002810
continue
2801-
podman_args = await container_to_args(
2802-
compose, cnt, detached=args.detach, no_deps=args.no_deps
2803-
)
2804-
subproc = await compose.podman.run([], podman_command, podman_args)
2805-
if podman_command == "run" and subproc is not None:
2811+
podman_args = await container_to_args(compose, cnt, detached=False, no_deps=args.no_deps)
2812+
subproc = await compose.podman.run([], "create", podman_args)
2813+
if not args.no_start and args.detach and subproc is not None:
28062814
await run_container(
28072815
compose, cnt["name"], deps_from_container(args, cnt), ([], "start", [cnt["name"]])
28082816
)
2817+
28092818
if args.no_start or args.detach or args.dry_run:
28102819
return
28112820
# TODO: handle already existing

tests/.pylintrc

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[MESSAGES CONTROL]
2+
# C0111 missing-docstring: missing-class-docstring, missing-function-docstring, missing-method-docstring, missing-module-docstrin
3+
# consider-using-with: we need it for color formatter pipe
4+
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,duplicate-code,missing-timeout
5+
# allow _ for ignored variables
6+
# allow generic names like a,b,c and i,j,k,l,m,n and x,y,z
7+
# allow k,v for key/value
8+
# allow e for exceptions, it for iterator, ix for index
9+
# allow ip for ip address
10+
# allow w,h for width, height
11+
# allow op for operation/operator/opcode
12+
# allow t, t0, t1, t2, and t3 for time
13+
# allow dt for delta time
14+
# allow db for database
15+
# allow ls for list
16+
# allow p for pipe
17+
# allow ex for examples, exists ..etc
18+
good-names=_,a,b,c,dt,db,e,f,fn,fd,i,j,k,v,kv,kw,l,m,n,ls,t,t0,t1,t2,t3,w,h,x,y,z,it,ix,ip,op,p,ex
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
version: "3.7"
2+
services:
3+
web:
4+
image: nopush/podman-compose-test
5+
command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-h", "/etc/", "-p", "8000"]
6+
tmpfs:
7+
- /run
8+
- /tmp
9+
healthcheck:
10+
test: ["CMD", "wget", "-qO-", "http://localhost:8000/hosts"]
11+
start_period: 10s # initialization time for containers that need time to bootstrap
12+
interval: 10s # Time between health checks
13+
timeout: 5s # Time to wait for a response
14+
retries: 3 # Number of consecutive failures before marking as unhealthy
15+
sleep:
16+
image: nopush/podman-compose-test
17+
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 3600"]
18+
depends_on:
19+
web:
20+
condition: service_healthy
21+
tmpfs:
22+
- /run
23+
- /tmp

tests/integration/deps/test_podman_compose_deps.py

+78-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# SPDX-License-Identifier: GPL-2.0
22
import os
3+
import time
34
import unittest
45

56
from tests.integration.test_utils import RunSubprocessMixin
7+
from tests.integration.test_utils import is_systemd_available
68
from tests.integration.test_utils import podman_compose_path
79
from tests.integration.test_utils import test_path
810

@@ -14,7 +16,7 @@ def compose_yaml_path(suffix=""):
1416
class TestComposeBaseDeps(unittest.TestCase, RunSubprocessMixin):
1517
def test_deps(self):
1618
try:
17-
output, error = self.run_subprocess_assert_returncode([
19+
output, _ = self.run_subprocess_assert_returncode([
1820
podman_compose_path(),
1921
"-f",
2022
compose_yaml_path(),
@@ -37,7 +39,7 @@ def test_deps(self):
3739

3840
def test_run_nodeps(self):
3941
try:
40-
output, error = self.run_subprocess_assert_returncode([
42+
output, _ = self.run_subprocess_assert_returncode([
4143
podman_compose_path(),
4244
"-f",
4345
compose_yaml_path(),
@@ -71,7 +73,7 @@ def test_up_nodeps(self):
7173
"--detach",
7274
"sleep",
7375
])
74-
output, error = self.run_subprocess_assert_returncode([
76+
output, _ = self.run_subprocess_assert_returncode([
7577
podman_compose_path(),
7678
"-f",
7779
compose_yaml_path(),
@@ -144,7 +146,7 @@ class TestComposeConditionalDeps(unittest.TestCase, RunSubprocessMixin):
144146
def test_deps_succeeds(self):
145147
suffix = "-conditional-succeeds"
146148
try:
147-
output, error = self.run_subprocess_assert_returncode([
149+
output, _ = self.run_subprocess_assert_returncode([
148150
podman_compose_path(),
149151
"-f",
150152
compose_yaml_path(suffix),
@@ -168,7 +170,7 @@ def test_deps_succeeds(self):
168170
def test_deps_fails(self):
169171
suffix = "-conditional-fails"
170172
try:
171-
output, error = self.run_subprocess_assert_returncode([
173+
output, _ = self.run_subprocess_assert_returncode([
172174
podman_compose_path(),
173175
"-f",
174176
compose_yaml_path(suffix),
@@ -183,3 +185,74 @@ def test_deps_fails(self):
183185
compose_yaml_path(suffix),
184186
"down",
185187
])
188+
189+
190+
class TestComposeConditionalDepsHealthy(unittest.TestCase, RunSubprocessMixin):
191+
def test_up_deps_healthy(self):
192+
suffix = "-conditional-healthy"
193+
try:
194+
self.run_subprocess_assert_returncode([
195+
podman_compose_path(),
196+
"-f",
197+
compose_yaml_path(suffix),
198+
"up",
199+
"sleep",
200+
"--detach",
201+
])
202+
203+
# The `podman wait --condition=healthy` is invalid prior to 4.6.0.
204+
# Since the podman-compose project uses podman 4.3.1 in github actions, we
205+
# use sleep as workaround to wait until the `sleep` container becomes running.
206+
time.sleep(3)
207+
208+
# self.run_subprocess_assert_returncode([
209+
# "podman",
210+
# "wait",
211+
# "--condition=running",
212+
# "deps_web_1",
213+
# "deps_sleep_1",
214+
# ])
215+
216+
# check both web and sleep are running
217+
output, _ = self.run_subprocess_assert_returncode([
218+
podman_compose_path(),
219+
"-f",
220+
compose_yaml_path(),
221+
"ps",
222+
"--format",
223+
"{{.ID}}\t{{.Names}}\t{{.Status}}\t{{.StartedAt}}",
224+
])
225+
# extract container id of web
226+
decoded_out = output.decode('utf-8')
227+
web_cnt_id = ""
228+
web_cnt_name = ""
229+
web_cnt_status = ""
230+
web_cnt_started = ""
231+
sleep_cnt_id = ""
232+
sleep_cnt_name = ""
233+
sleep_cnt_started = ""
234+
for line in decoded_out.split("\n"):
235+
if "web" in line:
236+
web_cnt_id, web_cnt_name, web_cnt_status, web_cnt_started = line.split("\t")
237+
if "sleep" in line:
238+
sleep_cnt_id, sleep_cnt_name, _, sleep_cnt_started = line.split("\t")
239+
self.assertNotEqual("", web_cnt_id)
240+
self.assertEqual("deps_web_1", web_cnt_name)
241+
self.assertNotEqual("", sleep_cnt_id)
242+
self.assertEqual("deps_sleep_1", sleep_cnt_name)
243+
if is_systemd_available():
244+
self.assertIn("healthy", web_cnt_status)
245+
else:
246+
# When test case is executed inside container like github actions, the absence of
247+
# systemd prevents health check from working properly, resulting in failure to
248+
# transit to healthy state. As a result, we only assert the `running` state
249+
self.assertIn("running", web_cnt_status)
250+
self.assertGreaterEqual(int(sleep_cnt_started), int(web_cnt_started))
251+
252+
finally:
253+
self.run_subprocess_assert_returncode([
254+
podman_compose_path(),
255+
"-f",
256+
compose_yaml_path(),
257+
"down",
258+
])

tests/integration/extends/test_podman_compose_extends.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -80,18 +80,25 @@ def test_extends_service_launch_env1(self):
8080
"env1",
8181
])
8282
lines = output.decode('utf-8').split('\n')
83-
# HOSTNAME name is random string so is ignored in asserting
84-
lines = sorted([line for line in lines if not line.startswith("HOSTNAME")])
83+
# Test selected env variables to improve robustness
84+
lines = sorted([
85+
line
86+
for line in lines
87+
if line.startswith("BAR")
88+
or line.startswith("BAZ")
89+
or line.startswith("FOO")
90+
or line.startswith("HOME")
91+
or line.startswith("PATH")
92+
or line.startswith("container")
93+
])
8594
self.assertEqual(
8695
lines,
8796
[
88-
'',
8997
'BAR=local',
9098
'BAZ=local',
9199
'FOO=original',
92100
'HOME=/root',
93101
'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
94-
'TERM=xterm',
95102
'container=podman',
96103
],
97104
)

tests/integration/filesystem/test_podman_compose_filesystem.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@ def test_compose_symlink(self):
3535
"container1",
3636
])
3737

38-
# BUG: figure out why cat is called twice
39-
self.assertEqual(out, b'data_compose_symlink\ndata_compose_symlink\n')
38+
self.assertEqual(out, b'data_compose_symlink\n')
4039

4140
finally:
4241
out, _ = self.run_subprocess_assert_returncode([

tests/integration/nets_test1/test_podman_compose_nets_test1.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,13 @@ def test_nets_test1(self):
5959
)
6060

6161
# check if Host port is the same as provided by the service port
62+
self.assertIsNotNone(container_info['NetworkSettings']["Ports"].get("8001/tcp", None))
63+
self.assertGreater(len(container_info['NetworkSettings']["Ports"]["8001/tcp"]), 0)
64+
self.assertIsNotNone(
65+
container_info['NetworkSettings']["Ports"]["8001/tcp"][0].get("HostPort", None)
66+
)
6267
self.assertEqual(
63-
container_info['NetworkSettings']["Ports"],
64-
{"8001/tcp": [{"HostIp": "", "HostPort": "8001"}]},
68+
container_info['NetworkSettings']["Ports"]["8001/tcp"][0]["HostPort"], "8001"
6569
)
6670

6771
self.assertEqual(container_info["Config"]["Hostname"], "web1")
@@ -77,9 +81,13 @@ def test_nets_test1(self):
7781
list(container_info["NetworkSettings"]["Networks"].keys())[0], "nets_test1_default"
7882
)
7983

84+
self.assertIsNotNone(container_info['NetworkSettings']["Ports"].get("8001/tcp", None))
85+
self.assertGreater(len(container_info['NetworkSettings']["Ports"]["8001/tcp"]), 0)
86+
self.assertIsNotNone(
87+
container_info['NetworkSettings']["Ports"]["8001/tcp"][0].get("HostPort", None)
88+
)
8089
self.assertEqual(
81-
container_info['NetworkSettings']["Ports"],
82-
{"8001/tcp": [{"HostIp": "", "HostPort": "8002"}]},
90+
container_info['NetworkSettings']["Ports"]["8001/tcp"][0]["HostPort"], "8002"
8391
)
8492

8593
self.assertEqual(container_info["Config"]["Hostname"], "web2")

tests/integration/nets_test2/test_podman_compose_nets_test2.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,13 @@ def test_nets_test2(self):
5959
)
6060

6161
# check if Host port is the same as prodvided by the service port
62+
self.assertIsNotNone(container_info['NetworkSettings']["Ports"].get("8001/tcp", None))
63+
self.assertGreater(len(container_info['NetworkSettings']["Ports"]["8001/tcp"]), 0)
64+
self.assertIsNotNone(
65+
container_info['NetworkSettings']["Ports"]["8001/tcp"][0].get("HostPort", None)
66+
)
6267
self.assertEqual(
63-
container_info['NetworkSettings']["Ports"],
64-
{"8001/tcp": [{"HostIp": "", "HostPort": "8001"}]},
68+
container_info['NetworkSettings']["Ports"]["8001/tcp"][0]["HostPort"], "8001"
6569
)
6670

6771
self.assertEqual(container_info["Config"]["Hostname"], "web1")
@@ -78,9 +82,13 @@ def test_nets_test2(self):
7882
list(container_info["NetworkSettings"]["Networks"].keys())[0], "nets_test2_mystack"
7983
)
8084

85+
self.assertIsNotNone(container_info['NetworkSettings']["Ports"].get("8001/tcp", None))
86+
self.assertGreater(len(container_info['NetworkSettings']["Ports"]["8001/tcp"]), 0)
87+
self.assertIsNotNone(
88+
container_info['NetworkSettings']["Ports"]["8001/tcp"][0].get("HostPort", None)
89+
)
8190
self.assertEqual(
82-
container_info['NetworkSettings']["Ports"],
83-
{"8001/tcp": [{"HostIp": "", "HostPort": "8002"}]},
91+
container_info['NetworkSettings']["Ports"]["8001/tcp"][0]["HostPort"], "8002"
8492
)
8593

8694
self.assertEqual(container_info["Config"]["Hostname"], "web2")

tests/integration/test_utils.py

+8
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ def podman_compose_path():
2121
return os.path.join(base_path(), "podman_compose.py")
2222

2323

24+
def is_systemd_available():
25+
try:
26+
with open("/proc/1/comm", "r", encoding="utf-8") as fh:
27+
return fh.read().strip() == "systemd"
28+
except FileNotFoundError:
29+
return False
30+
31+
2432
class RunSubprocessMixin:
2533
def is_debug_enabled(self):
2634
return "TESTS_DEBUG" in os.environ

0 commit comments

Comments
 (0)