diff --git a/containercrop/github_api.py b/containercrop/github_api.py index 48392e9..8adab3c 100644 --- a/containercrop/github_api.py +++ b/containercrop/github_api.py @@ -164,7 +164,7 @@ async def get_versions(self, image_name: str) -> list[Image]: async def delete_image(self, image: Image) -> bool: "Delete an image" if not image.url: - logging.info("Could not delete image ad it does not have an url: %s", image) + logging.info("Could not delete image as it does not have an url: %s", image) async with self.session.delete(image.url) as resp: # type: ignore if resp.status == 204: return True diff --git a/containercrop/retention.py b/containercrop/retention.py index 0dac900..cc9b58f 100644 --- a/containercrop/retention.py +++ b/containercrop/retention.py @@ -89,6 +89,7 @@ def matches_retention_policy(image: Image, args: RetentionArgs) -> bool: ): logging.debug("Image %s(%s) does match skip tags", image.name, image.html_url) return False + if args.untagged_only and image.tags: logging.debug( "Image %s(%s) is tagged and untagged_only is set", @@ -96,15 +97,18 @@ def matches_retention_policy(image: Image, args: RetentionArgs) -> bool: image.html_url, ) return False - if args.cut_off and image.is_before_cut_off_date(args.cut_off): - logging.debug("Image %s(%s) is before cut-off date", image.name, image.html_url) - return True - if args.filter_tags and any( + + if args.filter_tags and not any( any(fnmatch(tag, filter_tag) for filter_tag in args.filter_tags) for tag in image.tags ): logging.debug("Image %s(%s) does match filter tags", image.name, image.html_url) + return False + + if args.cut_off and image.is_before_cut_off_date(args.cut_off): + logging.debug("Image %s(%s) is before cut-off date", image.name, image.html_url) return True + logging.debug( "Image %s(%s) does not match any policy, therefore we keep it", image.name, @@ -117,6 +121,7 @@ def apply_retention_policy(args: RetentionArgs, images: list[Image]) -> list[Ima """ Apply the retention policy to the images and return the ones that should be deleted. """ + images.sort(key=lambda x: x.updated_at, reverse=True) # delete old images first matches = [image for image in images if matches_retention_policy(image, args)] return matches[args.keep_at_least :] diff --git a/containercrop/test_retention.py b/containercrop/test_retention.py index f418897..66bad25 100644 --- a/containercrop/test_retention.py +++ b/containercrop/test_retention.py @@ -111,6 +111,26 @@ def test_delete_untagged_images_older_than_one_day(): def test_filter_by_specific_tags_for_deletion(generate_images): + images = generate_images( + tags_list=[["v1"], ["v1.0"], ["beta"], ["latest"]], + days_old_list=[30], + names_list=["image1"], + ) + policy = RetentionArgs( + image_name="image1", + cut_off="20 days ago UTC", + untagged_only=False, + skip_tags="", + keep_at_least=0, + filter_tags="v1,v1.0", + dry_run=True, + token="dummy_token", + repo_owner="test", + ) + assert len(apply_retention_policy(policy, images)) == 2 + + +def test_filter_by_specific_tags_for_deletion_but_not_expired(generate_images): images = generate_images( tags_list=[["v1"], ["v1.0"], ["beta"], ["latest"]], days_old_list=[5, 5, 5, 5], @@ -127,6 +147,69 @@ def test_filter_by_specific_tags_for_deletion(generate_images): token="dummy_token", repo_owner="test", ) + assert len(apply_retention_policy(policy, images)) == 0 + + +def test_filter_by_specific_tags_for_deletion_when_all_are_expired(generate_images): + images = generate_images( + tags_list=[["v1"], ["v1.0"], ["beta"], ["latest"]], + days_old_list=[30], + names_list=["image1"], + ) + policy = RetentionArgs( + image_name="image1", + cut_off="20 days ago UTC", + untagged_only=False, + skip_tags="", + keep_at_least=0, + filter_tags="v1,v1.0", + dry_run=True, + token="dummy_token", + repo_owner="test", + ) + assert len(apply_retention_policy(policy, images)) == 2 + + +def test_filter_by_specific_tags_for_deletion_when_there_are_multiple_tags( + generate_images, +): + images = generate_images( + tags_list=[["asdf", "v1"], ["test", "test1", "v1.0"], ["beta"], ["latest"]], + days_old_list=[30], + names_list=["image1"], + ) + policy = RetentionArgs( + image_name="image1", + cut_off="20 days ago UTC", + untagged_only=False, + skip_tags="", + keep_at_least=0, + filter_tags="v1,v1.0", + dry_run=True, + token="dummy_token", + repo_owner="test", + ) + assert len(apply_retention_policy(policy, images)) == 2 + + +def test_filter_by_wildcard_tags_for_deletion( + generate_images, +): + images = generate_images( + tags_list=[["asdf", "v1"], ["test", "test1", "v1.0"], ["beta"], ["latest"]], + days_old_list=[30], + names_list=["image1"], + ) + policy = RetentionArgs( + image_name="image1", + cut_off="20 days ago UTC", + untagged_only=False, + skip_tags="", + keep_at_least=0, + filter_tags="v1*", + token="dummy_token", + repo_owner="test", + ) assert len(apply_retention_policy(policy, images)) == 2 @@ -143,7 +226,6 @@ def test_skip_specific_tags(generate_images): skip_tags="beta,latest", keep_at_least=0, filter_tags="", - dry_run=True, token="dummy_token", repo_owner="test", ) @@ -183,7 +265,6 @@ def test_delete_untagged_images_older_than_x_days(generate_images): cut_off="2 days ago UTC", untagged_only=True, skip_tags="", - keep_at_least=0, filter_tags="", repo_owner="test", ) @@ -205,7 +286,6 @@ def test_delete_tagged_keep_recent_untagged(generate_images): cut_off="5 days ago UTC", untagged_only=False, skip_tags="", - keep_at_least=0, filter_tags="", repo_owner="test", ) @@ -270,7 +350,6 @@ def test_wildcard_tag_filtering(generate_images): cut_off="3 days ago UTC", untagged_only=False, skip_tags="v1.*", # Skip any version starting with v1. - keep_at_least=0, filter_tags="*", # Consider all tags repo_owner="test", ) @@ -281,3 +360,24 @@ def test_wildcard_tag_filtering(generate_images): assert not any( "v1" in tag for img in retained_images for tag in img.tags ) # No v1.* tags + + +def test_image_processing_enforces_date_order(generate_images): + images = generate_images( + tags_list=[["v1.1"], ["v1.2"], ["beta"], ["latest"]], + days_old_list=[35, 34, 33, 32], + names_list=["image1"], + ) + policy = RetentionArgs( + image_name="image1", + cut_off="30 days ago UTC", + repo_owner="test", + keep_at_least=2, + skip_tags=[], + ) + retained_images = apply_retention_policy(policy, images) + assert ( + len(retained_images) == 2 + ) # "beta" and possibly "latest" if not matching skip_tags wildcard + assert retained_images[0].tags == ["v1.2"] + assert retained_images[1].tags == ["v1.1"]