diff --git a/podman/domain/images_manager.py b/podman/domain/images_manager.py index 0ebf57c5..416b21dd 100644 --- a/podman/domain/images_manager.py +++ b/podman/domain/images_manager.py @@ -135,24 +135,39 @@ def load(self, data: bytes) -> Generator[Image, None, None]: yield self.get(item) def prune( - self, filters: Optional[Mapping[str, Any]] = None + self, + all: Optional[bool] = False, # pylint: disable=redefined-builtin + external: Optional[bool] = False, + filters: Optional[Mapping[str, Any]] = None, ) -> Dict[Literal["ImagesDeleted", "SpaceReclaimed"], Any]: """Delete unused images. The Untagged keys will always be "". Args: + all: Remove all images not in use by containers, not just dangling ones. + external: Remove images even when they are used by external containers + (e.g, by build containers). filters: Qualify Images to prune. Available filters: - dangling (bool): when true, only delete unused and untagged images. + - label: (dict): filter by label. + Examples: + filters={"label": {"key": "value"}} + filters={"label!": {"key": "value"}} - until (str): Delete images older than this timestamp. Raises: APIError: when service returns an error """ - response = self.client.post( - "/images/prune", params={"filters": api.prepare_filters(filters)} - ) + + params = { + "all": all, + "external": external, + "filters": api.prepare_filters(filters), + } + + response = self.client.post("/images/prune", params=params) response.raise_for_status() deleted: List[Dict[str, str]] = [] diff --git a/podman/tests/unit/test_imagesmanager.py b/podman/tests/unit/test_imagesmanager.py index 6de8910f..dbe648ae 100644 --- a/podman/tests/unit/test_imagesmanager.py +++ b/podman/tests/unit/test_imagesmanager.py @@ -207,6 +207,64 @@ def test_prune_filters(self, mock): self.assertEqual(len(untagged), 2) self.assertEqual(len("".join(untagged)), 0) + @requests_mock.Mocker() + def test_prune_filters_label(self, mock): + """Unit test filters param label for Images prune().""" + mock.post( + tests.LIBPOD_URL + + "/images/prune?filters=%7B%22label%22%3A+%5B%22%7B%27license%27%3A+%27Apache-2.0%27%7D%22%5D%7D", + json=[ + { + "Id": "326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab", + "Size": 1024, + }, + ], + ) + + report = self.client.images.prune(filters={"label": {"license": "Apache-2.0"}}) + self.assertIn("ImagesDeleted", report) + self.assertIn("SpaceReclaimed", report) + + self.assertEqual(report["SpaceReclaimed"], 1024) + + deleted = [r["Deleted"] for r in report["ImagesDeleted"] if "Deleted" in r] + self.assertEqual(len(deleted), 1) + self.assertIn("326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab", deleted) + self.assertGreater(len("".join(deleted)), 0) + + untagged = [r["Untagged"] for r in report["ImagesDeleted"] if "Untagged" in r] + self.assertEqual(len(untagged), 1) + self.assertEqual(len("".join(untagged)), 0) + + @requests_mock.Mocker() + def test_prune_filters_not_label(self, mock): + """Unit test filters param NOT-label for Images prune().""" + mock.post( + tests.LIBPOD_URL + + "/images/prune?filters=%7B%22label%21%22%3A+%5B%22%7B%27license%27%3A+%27Apache-2.0%27%7D%22%5D%7D", + json=[ + { + "Id": "c4b16966ecd94ffa910eab4e630e24f259bf34a87e924cd4b1434f267b0e354e", + "Size": 1024, + }, + ], + ) + + report = self.client.images.prune(filters={"label!": {"license": "Apache-2.0"}}) + self.assertIn("ImagesDeleted", report) + self.assertIn("SpaceReclaimed", report) + + self.assertEqual(report["SpaceReclaimed"], 1024) + + deleted = [r["Deleted"] for r in report["ImagesDeleted"] if "Deleted" in r] + self.assertEqual(len(deleted), 1) + self.assertIn("c4b16966ecd94ffa910eab4e630e24f259bf34a87e924cd4b1434f267b0e354e", deleted) + self.assertGreater(len("".join(deleted)), 0) + + untagged = [r["Untagged"] for r in report["ImagesDeleted"] if "Untagged" in r] + self.assertEqual(len(untagged), 1) + self.assertEqual(len("".join(untagged)), 0) + @requests_mock.Mocker() def test_prune_failure(self, mock): """Unit test to report error carried in response body."""