Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions newsfragments/add_rm_command.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `rm` command to remove stopped service containers, matching `docker-compose rm` behavior.
43 changes: 42 additions & 1 deletion podman_compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -4526,6 +4526,24 @@ async def compose_down(compose: PodmanCompose, args: argparse.Namespace) -> None
await compose.podman.run([], "network", ["rm", network])


@cmd_run(podman_compose, "rm", "remove stopped service containers")
async def compose_rm(compose: PodmanCompose, args: argparse.Namespace) -> None:
services = set(args.services) if args.services else set(compose.services)
containers = list(reversed(compose.containers))

for cnt in containers:
if cnt["_service"] not in services:
continue
if args.stop:
await compose.podman.run([], "stop", ["-i", cnt["name"]])
rm_args = []
if args.force:
rm_args.append("-f")
if args.volumes:
rm_args.append("-v")
await compose.podman.run([], "rm", [*rm_args, cnt["name"]])


@cmd_run(podman_compose, "ps", "show status of containers")
async def compose_ps(compose: PodmanCompose, args: argparse.Namespace) -> None:
ps_args = ["-a", "--filter", f"label=io.podman.compose.project={compose.project_name}"]
Expand Down Expand Up @@ -5088,6 +5106,29 @@ def compose_down_parse(parser: argparse.ArgumentParser) -> None:
)


@cmd_parse(podman_compose, "rm")
def compose_rm_parse(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"-s",
"--stop",
action="store_true",
help="Stop the containers, if required, before removing.",
)
parser.add_argument(
"-f",
"--force",
action="store_true",
help="Force removal of running or unusable containers. podman-compose has no "
"interactive prompts so behaves differently than docker-compose here.",
)
parser.add_argument(
"-v",
"--volumes",
action="store_true",
help="Remove any anonymous volumes attached to containers.",
)


@cmd_parse(podman_compose, "run")
def compose_run_parse(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
Expand Down Expand Up @@ -5384,7 +5425,7 @@ def compose_build_up_parse(parser: argparse.ArgumentParser) -> None:
)


@cmd_parse(podman_compose, ["build", "up", "down", "start", "stop", "restart"])
@cmd_parse(podman_compose, ["build", "up", "down", "start", "stop", "restart", "rm"])
def compose_build_parse(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"services",
Expand Down
Empty file.
12 changes: 12 additions & 0 deletions tests/integration/compose_rm_behavior/docker-compose_default.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
services:
app:
image: nopush/podman-compose-test
command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-p", "8080"]
depends_on:
- db
db:
image: nopush/podman-compose-test
command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-p", "8080"]
no_deps:
image: nopush/podman-compose-test
command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-p", "8080"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
services:
app_with_vol:
image: nopush/podman-compose-test
command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-p", "8080"]
volumes:
- /data
228 changes: 228 additions & 0 deletions tests/integration/compose_rm_behavior/test_compose_rm_behavior.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import os
import unittest

from parameterized import parameterized

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(scenario: str) -> str:
return os.path.join(
os.path.join(test_path(), "compose_rm_behavior"), f"docker-compose_{scenario}.yaml"
)


class TestComposeRmBehavior(unittest.TestCase, RunSubprocessMixin):
@parameterized.expand([
("default", ["rm"], set()),
(
"default",
["rm", "app"],
{
"compose_rm_behavior_db_1",
"compose_rm_behavior_no_deps_1",
},
),
(
"default",
["rm", "no_deps"],
{
"compose_rm_behavior_app_1",
"compose_rm_behavior_db_1",
},
),
])
def test_compose_rm(
self, scenario: str, command_args: list[str], expect_remaining: set[str]
) -> None:
try:
self.run_subprocess_assert_returncode(
[podman_compose_path(), "-f", compose_yaml_path(scenario), "up", "-d"],
)

# stop containers before rm (rm only removes stopped containers by default)
self.run_subprocess_assert_returncode(
[podman_compose_path(), "-f", compose_yaml_path(scenario), "stop"],
)

self.run_subprocess_assert_returncode(
[
podman_compose_path(),
"-f",
compose_yaml_path(scenario),
*command_args,
],
)

out, _ = self.run_subprocess_assert_returncode(
[
podman_compose_path(),
"-f",
compose_yaml_path(scenario),
"ps",
"--format",
'{{ .Names }}',
],
)

actual_containers = set()
for line in out.decode('utf-8').strip().split('\n'):
name = line.strip()
if name:
actual_containers.add(name)

self.assertEqual(actual_containers, expect_remaining)
finally:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(scenario),
"down",
"-t",
"0",
])

def test_compose_rm_stop_flag(self) -> None:
scenario = "default"
try:
self.run_subprocess_assert_returncode(
[podman_compose_path(), "-f", compose_yaml_path(scenario), "up", "-d"],
)

# rm --stop should stop and remove running containers
self.run_subprocess_assert_returncode(
[
podman_compose_path(),
"-f",
compose_yaml_path(scenario),
"rm",
"-s",
],
)

out, _ = self.run_subprocess_assert_returncode(
[
podman_compose_path(),
"-f",
compose_yaml_path(scenario),
"ps",
"--format",
'{{ .Names }}',
],
)

actual_containers = set()
for line in out.decode('utf-8').strip().split('\n'):
name = line.strip()
if name:
actual_containers.add(name)

self.assertEqual(actual_containers, set())
finally:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(scenario),
"down",
"-t",
"0",
])

def test_compose_rm_force_flag(self) -> None:
scenario = "default"
try:
self.run_subprocess_assert_returncode(
[podman_compose_path(), "-f", compose_yaml_path(scenario), "up", "-d"],
)

# rm -f should force remove running containers
self.run_subprocess_assert_returncode(
[
podman_compose_path(),
"-f",
compose_yaml_path(scenario),
"rm",
"-f",
],
)

out, _ = self.run_subprocess_assert_returncode(
[
podman_compose_path(),
"-f",
compose_yaml_path(scenario),
"ps",
"--format",
'{{ .Names }}',
],
)

actual_containers = set()
for line in out.decode('utf-8').strip().split('\n'):
name = line.strip()
if name:
actual_containers.add(name)

self.assertEqual(actual_containers, set())
finally:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(scenario),
"down",
"-t",
"0",
])

def test_compose_rm_volumes_flag(self) -> None:
scenario = "volumes"
try:
self.run_subprocess_assert_returncode(
[podman_compose_path(), "-f", compose_yaml_path(scenario), "up", "-d"],
)

# stop containers before rm (rm only removes stopped containers by default)
self.run_subprocess_assert_returncode(
[podman_compose_path(), "-f", compose_yaml_path(scenario), "stop"],
)

# rm -v should remove anonymous volumes attached to containers
self.run_subprocess_assert_returncode(
[
podman_compose_path(),
"-f",
compose_yaml_path(scenario),
"rm",
"-v",
],
)

out, _ = self.run_subprocess_assert_returncode(
[
podman_compose_path(),
"-f",
compose_yaml_path(scenario),
"ps",
"--format",
'{{ .Names }}',
],
)

actual_containers = set()
for line in out.decode('utf-8').strip().split('\n'):
name = line.strip()
if name:
actual_containers.add(name)

self.assertEqual(actual_containers, set())
finally:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(scenario),
"down",
"-t",
"0",
])
Loading