Skip to content

Commit ffe2a29

Browse files
authored
Merge pull request #1283 from lisongmin/fix-stop-wrong-dependents-on-compose-down
fix: podman-compose down should not stop the upstream dependencies.
2 parents 5b0ee7c + cd25efa commit ffe2a29

File tree

6 files changed

+171
-3
lines changed

6 files changed

+171
-3
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix `podman-compose down service` stops wrong dependents

podman_compose.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1433,6 +1433,16 @@ def rec_deps(
14331433
return deps
14341434

14351435

1436+
def calc_dependents(services: dict[str, Any]) -> None:
1437+
for name, srv in services.items():
1438+
deps: set[ServiceDependency] = srv.get("_deps", set())
1439+
for dep in deps:
1440+
if dep.name in services:
1441+
services[dep.name].setdefault(DependField.DEPENDENTS, set()).add(
1442+
ServiceDependency(name, dep.condition.value)
1443+
)
1444+
1445+
14361446
def flat_deps(services: dict[str, Any], with_extends: bool = False) -> None:
14371447
"""
14381448
create dependencies "_deps" or update it recursively for all services
@@ -1470,6 +1480,8 @@ def flat_deps(services: dict[str, Any], with_extends: bool = False) -> None:
14701480
for name, srv in services.items():
14711481
rec_deps(services, name)
14721482

1483+
calc_dependents(services)
1484+
14731485

14741486
###################
14751487
# Override and reset tags
@@ -3024,14 +3036,23 @@ async def create_pods(compose: PodmanCompose) -> None:
30243036
await compose.podman.run([], "pod", podman_args)
30253037

30263038

3027-
def get_excluded(compose: PodmanCompose, args: argparse.Namespace) -> set[str]:
3039+
class DependField(str, Enum):
3040+
DEPENDENCIES = "_deps"
3041+
DEPENDENTS = "_dependents"
3042+
3043+
3044+
def get_excluded(
3045+
compose: PodmanCompose,
3046+
args: argparse.Namespace,
3047+
dep_field: DependField = DependField.DEPENDENCIES,
3048+
) -> set[str]:
30283049
excluded = set()
30293050
if args.services:
30303051
excluded = set(compose.services)
30313052
for service in args.services:
30323053
# we need 'getattr' as compose_down_parse dose not configure 'no_deps'
30333054
if service in compose.services and not getattr(args, "no_deps", False):
3034-
excluded -= set(x.name for x in compose.services[service]["_deps"])
3055+
excluded -= set(x.name for x in compose.services[service].get(dep_field, set()))
30353056
excluded.discard(service)
30363057
log.debug("** excluding: %s", excluded)
30373058
return excluded
@@ -3322,7 +3343,7 @@ def get_volume_names(compose: PodmanCompose, cnt: dict) -> list[str]:
33223343

33233344
@cmd_run(podman_compose, "down", "tear down entire stack")
33243345
async def compose_down(compose: PodmanCompose, args: argparse.Namespace) -> None:
3325-
excluded = get_excluded(compose, args)
3346+
excluded = get_excluded(compose, args, DependField.DEPENDENTS)
33263347
podman_args: list[str] = []
33273348
timeout_global = getattr(args, "timeout", None)
33283349
containers = list(reversed(compose.containers))

tests/integration/compose_down_behavior/__init__.py

Whitespace-only changes.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
services:
2+
app:
3+
image: nopush/podman-compose-test
4+
command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-p", "8080"]
5+
depends_on:
6+
- db
7+
db:
8+
image: nopush/podman-compose-test
9+
command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-p", "8080"]
10+
no_deps:
11+
image: nopush/podman-compose-test
12+
command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-p", "8080"]
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# SPDX-License-Identifier: GPL-2.0
2+
3+
import os
4+
import unittest
5+
6+
from parameterized import parameterized
7+
8+
from tests.integration.test_utils import RunSubprocessMixin
9+
from tests.integration.test_utils import podman_compose_path
10+
from tests.integration.test_utils import test_path
11+
12+
13+
def compose_yaml_path(scenario: str) -> str:
14+
return os.path.join(
15+
os.path.join(test_path(), "compose_down_behavior"), f"docker-compose_{scenario}.yaml"
16+
)
17+
18+
19+
class TestComposeDownBehavior(unittest.TestCase, RunSubprocessMixin):
20+
@parameterized.expand([
21+
("default", ["down"], set()),
22+
(
23+
"default",
24+
["down", "app"],
25+
{
26+
"compose_down_behavior_db_1",
27+
"compose_down_behavior_no_deps_1",
28+
},
29+
),
30+
(
31+
"default",
32+
["down", "db"],
33+
{
34+
"compose_down_behavior_no_deps_1",
35+
},
36+
),
37+
])
38+
def test_compose_down(
39+
self, scenario: str, command_args: list[str], expect_containers: set[str]
40+
) -> None:
41+
try:
42+
self.run_subprocess_assert_returncode(
43+
[podman_compose_path(), "-f", compose_yaml_path(scenario), "up", "-d"],
44+
)
45+
46+
self.run_subprocess_assert_returncode(
47+
[
48+
podman_compose_path(),
49+
"-f",
50+
compose_yaml_path(scenario),
51+
*command_args,
52+
],
53+
)
54+
55+
out, _ = self.run_subprocess_assert_returncode(
56+
[
57+
podman_compose_path(),
58+
"-f",
59+
compose_yaml_path(scenario),
60+
"ps",
61+
"--format",
62+
'{{ .Names }}',
63+
],
64+
)
65+
66+
actual_containers = set()
67+
for line in out.decode('utf-8').strip().split('\n'):
68+
name = line.strip()
69+
if name:
70+
actual_containers.add(name)
71+
72+
self.assertEqual(actual_containers, expect_containers)
73+
finally:
74+
self.run_subprocess_assert_returncode([
75+
podman_compose_path(),
76+
"-f",
77+
compose_yaml_path(scenario),
78+
"down",
79+
"-t",
80+
"0",
81+
])

tests/unit/test_depends_on.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import unittest
2+
from typing import Any
3+
4+
from parameterized import parameterized
5+
6+
from podman_compose import flat_deps
7+
8+
9+
class TestDependsOn(unittest.TestCase):
10+
@parameterized.expand([
11+
(
12+
{
13+
"service_a": {},
14+
"service_b": {"depends_on": {"service_a": {"condition": "healthy"}}},
15+
"service_c": {"depends_on": {"service_b": {"condition": "healthy"}}},
16+
},
17+
# dependencies
18+
{
19+
"service_a": set(),
20+
"service_b": set(["service_a"]),
21+
"service_c": set(["service_a", "service_b"]),
22+
},
23+
# dependents
24+
{
25+
"service_a": set(["service_b", "service_c"]),
26+
"service_b": set(["service_c"]),
27+
"service_c": set(),
28+
},
29+
),
30+
])
31+
def test_flat_deps(
32+
self,
33+
services: dict[str, Any],
34+
deps: dict[str, set[str]],
35+
dependents: dict[str, set[str]],
36+
) -> None:
37+
flat_deps(services)
38+
self.assertEqual(
39+
{
40+
name: set([x.name for x in value.get("_deps", set())])
41+
for name, value in services.items()
42+
},
43+
deps,
44+
msg="Dependencies do not match",
45+
)
46+
self.assertEqual(
47+
{
48+
name: set([x.name for x in value.get("_dependents", set())])
49+
for name, value in services.items()
50+
},
51+
dependents,
52+
msg="Dependents do not match",
53+
)

0 commit comments

Comments
 (0)