diff --git a/podman_compose.py b/podman_compose.py index 46725ffd..d878e193 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -63,6 +63,8 @@ def filteri(a: list[str]) -> list[str]: @overload def try_int(i: int | str, fallback: int) -> int: ... + + @overload def try_int(i: int | str, fallback: None) -> int | None: ... @@ -272,8 +274,12 @@ def fix_mount_dict( @overload def rec_subs(value: dict, subs_dict: dict[str, Any]) -> dict: ... + + @overload def rec_subs(value: str, subs_dict: dict[str, Any]) -> str: ... + + @overload def rec_subs(value: Iterable, subs_dict: dict[str, Any]) -> Iterable: ... @@ -2734,6 +2740,63 @@ def wrapped(*args: Any, **kw: Any) -> Any: ################### +@cmd_run(podman_compose, "ls", "List running compose projects") +async def list_running_projects(compose: PodmanCompose, args: argparse.Namespace) -> None: + img_containers = [cnt for cnt in compose.containers if "image" in cnt] + parsed_args = vars(args) + _format = parsed_args.get("format", "table") + data: list[Any] = [] + if _format == "table": + data.append(["NAME", "STATUS", "CONFIG_FILES"]) + + for img in img_containers: + try: + name = img["name"] + output = await compose.podman.output( + [], + "inspect", + [ + name, + "--format", + ''' + {{ .State.Status }} + {{ .State.Running }} + {{ index .Config.Labels "com.docker.compose.project.working_dir" }} + {{ index .Config.Labels "com.docker.compose.project.config_files" }} + ''', + ], + ) + command_output = output.decode().split() + running = bool(json.loads(command_output[1])) + status = "{}({})".format(command_output[0], 1 if running else 0) + path = "{}/{}".format(command_output[2], command_output[3]) + + if _format == "table": + if isinstance(command_output, list): + data.append([name, status, path]) + + elif _format == "json": + # Replicate how docker compose returns the list + json_obj = {"Name": name, "Status": status, "ConfigFiles": path} + data.append(json_obj) + except Exception: + break + + if _format == "table": + # Determine the maximum length of each column + column_widths = [max(map(len, column)) for column in zip(*data)] + + # Print each row + for row in data: + # Format each cell using the maximum column width + formatted_row = [cell.ljust(width) for cell, width in zip(row, column_widths)] + formatted_row[-2:] = ["\t".join(formatted_row[-2:]).strip()] + print("\t".join(formatted_row)) + + elif _format == "json": + print(data) + + @cmd_run(podman_compose, "version", "show version") async def compose_version(compose: PodmanCompose, args: argparse.Namespace) -> None: if getattr(args, "short", False): @@ -4381,6 +4444,17 @@ def compose_format_parse(parser: argparse.ArgumentParser) -> None: ) +@cmd_parse(podman_compose, "ls") +def compose_ls_parse(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "-f", + "--format", + choices=["table", "json"], + default="table", + help="Format the output", + ) + + async def async_main() -> None: await podman_compose.run() diff --git a/tests/integration/list/__init__.py b/tests/integration/list/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/list/docker-compose.yml b/tests/integration/list/docker-compose.yml new file mode 100644 index 00000000..99384f3e --- /dev/null +++ b/tests/integration/list/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3.6' + +services: + service_1: + image: nopush/podman-compose-test + command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-h", ".", "-p", "8003"] + service_2: + image: nopush/podman-compose-test + command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-h", ".", "-p", "8003"] + service_3: + image: nopush/podman-compose-test + command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-h", ".", "-p", "8003"] + diff --git a/tests/integration/list/test_podman_compose_list.py b/tests/integration/list/test_podman_compose_list.py new file mode 100644 index 00000000..be5af27e --- /dev/null +++ b/tests/integration/list/test_podman_compose_list.py @@ -0,0 +1,102 @@ +import ast +import os +import unittest +from typing import Union + +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 TestPodmanComposeInclude(unittest.TestCase, RunSubprocessMixin): + def test_podman_compose_list(self) -> None: + """ + Test podman compose list (ls) command + """ + command_up = [ + "coverage", + "run", + podman_compose_path(), + "-f", + os.path.join(test_path(), "list", "docker-compose.yml"), + "up", + "-d", + ] + + command_list = [ + "coverage", + "run", + podman_compose_path(), + "-f", + os.path.join(test_path(), "list", "docker-compose.yml"), + "ls", + ] + + command_check_container = [ + "podman", + "ps", + "-a", + "--filter", + "label=io.podman.compose.project=list", + "--format", + '"{{.Image}}"', + ] + + command_container_id = [ + "podman", + "ps", + "-a", + "--filter", + "label=io.podman.compose.project=list", + "--format", + '"{{.ID}}"', + ] + + command_down = ["podman", "rm", "--force"] + service: Union[dict[str, str], str] + + running_containers = [] + self.run_subprocess_assert_returncode(command_up) + out, _ = self.run_subprocess_assert_returncode(command_list) + str_out = out.decode() + + # Test for table view + services = str_out.strip().split("\n") + headers = [h.strip() for h in services[0].split("\t")] + + for service in services[1:]: + values = [val.strip() for val in service.split("\t")] + zipped = dict(zip(headers, values)) + self.assertNotEqual(zipped.get("NAME"), None) + self.assertNotEqual(zipped.get("STATUS"), None) + self.assertNotEqual(zipped.get("CONFIG_FILES"), None) + running_containers.append(zipped) + self.assertEqual(len(running_containers), 3) + + # Test for json view + command_list.extend(["--format", "json"]) + out, _ = self.run_subprocess_assert_returncode(command_list) + str_out = out.decode() + json_services: list[dict] = ast.literal_eval(str_out) + self.assertIsInstance(json_services, list) + + for service in json_services: + self.assertIsInstance(service, dict) + self.assertNotEqual(service.get("Name"), None) + self.assertNotEqual(service.get("Status"), None) + self.assertNotEqual(service.get("ConfigFiles"), None) + + self.assertEqual(len(json_services), 3) + + # Get container ID to remove it + out, _ = self.run_subprocess_assert_returncode(command_container_id) + self.assertNotEqual(out, b"") + container_ids = out.decode().strip().split("\n") + container_ids = [container_id.replace('"', "") for container_id in container_ids] + command_down.extend(container_ids) + out, _ = self.run_subprocess_assert_returncode(command_down) + # cleanup test image(tags) + self.assertNotEqual(out, b"") + # check container did not exists anymore + out, _ = self.run_subprocess_assert_returncode(command_check_container) + self.assertEqual(out, b"")