diff --git a/registry.py b/registry.py index a365560..8326d3c 100755 --- a/registry.py +++ b/registry.py @@ -546,6 +546,12 @@ for more detail on garbage collection read here: default='POST', metavar="POST|GET" ) + parser.add_argument( + '--order-by-date', + help=('Orders images by date instead of by tag name.' + 'Useful if your tag names are not in a fixed order.'), + action='store_true' + ) return parser.parse_args(args) @@ -672,6 +678,29 @@ def get_newer_tags(registry, image_name, hours, tags_list): return result +def get_datetime_tags(registry, image_name, tags_list): + def newer(tag): + image_config = registry.get_tag_config(image_name, tag) + if image_config == []: + print("tag not found") + return None + image_age = registry.get_image_age(image_name, image_config) + if image_age == []: + print("timestamp not found") + return None + return { + "tag": tag, + "datetime": dt.strptime(image_age[:-4], "%Y-%m-%dT%H:%M:%S.%f") + } + + print('---------------------------------') + p = ThreadPool(4) + result = list(x for x in p.map(newer, tags_list) if x) + p.close() + p.join() + return result + + def keep_images_like(image_list, regexp_list): if image_list is None or regexp_list is None: return [] @@ -685,6 +714,18 @@ def keep_images_like(image_list, regexp_list): return result +def get_ordered_tags(registry, image_name, tags_list, order_by_date=False): + if order_by_date: + tags_date = get_datetime_tags(registry, image_name, tags_list) + sorted_tags_by_date = sorted( + tags_date, + key=lambda x: x["datetime"] + ) + return [x["tag"] for x in sorted_tags_by_date] + + return sorted(tags_list, key=natural_keys) + + def main_loop(args): global DEBUG @@ -778,8 +819,8 @@ def main_loop(args): if args.delete_all: tags_list_to_delete = list(tags_list) else: - tags_list_to_delete = sorted(tags_list, key=natural_keys)[ - :-keep_last_versions] + ordered_tags_list = get_ordered_tags(registry, image_name, tags_list, args.order_by_date) + tags_list_to_delete = ordered_tags_list[:-keep_last_versions] # A manifest might be shared between different tags. Explicitly add those # tags that we want to preserve to the keep_tags list, to prevent diff --git a/test.py b/test.py index 7f616f4..7c5e843 100644 --- a/test.py +++ b/test.py @@ -1,7 +1,10 @@ import unittest + +from datetime import datetime + from registry import Registry, Requests, get_tags, parse_args, \ delete_tags, delete_tags_by_age, get_error_explanation, get_newer_tags, \ - keep_images_like, main_loop + keep_images_like, main_loop, get_datetime_tags, get_ordered_tags from mock import MagicMock, patch import requests @@ -715,6 +718,72 @@ class TestGetNewerTags(unittest.TestCase): ) +class TestGetDatetimeTags(unittest.TestCase): + + def setUp(self): + self.registry = Registry() + self.registry.http = MockRequests() + + self.get_tag_config_mock = MagicMock(return_value={'mediaType': 'application/vnd.docker.container.image.v1+json', 'size': 12953, + 'digest': 'sha256:8d71dfbf239c0015ad66993d55d3954cee2d52d86f829fdff9ccfb9f23b75aa8'}) + self.registry.get_tag_config = self.get_tag_config_mock + self.get_image_age_mock = MagicMock( + return_value="2017-12-27T12:47:33.511765448Z") + self.registry.get_image_age = self.get_image_age_mock + self.list_tags_mock = MagicMock(return_value=["image"]) + self.registry.list_tags = self.list_tags_mock + self.get_tag_digest_mock = MagicMock() + self.registry.get_tag_digest = self.get_tag_digest_mock + self.registry.http = MockRequests() + self.registry.hostname = "http://testdomain.com" + self.registry.http.reset_return_value(200, "MOCK_DIGEST") + + def test_get_datetime_tags(self): + self.assertEqual( + get_datetime_tags(self.registry, "imagename", ["latest"]), + [{"tag": "latest", "datetime": datetime(2017, 12, 27, 12, 47, 33, 511765)}] + ) + + +class TestGetOrderedTags(unittest.TestCase): + def setUp(self): + self.tags = ["e61d48b", "ff24a83", "ddd514c", "f4ba381", "9d5fab2"] + + def test_tags_are_ordered_by_name_by_default(self): + tags = ["v1", "v10", "v2"] + ordered_tags = get_ordered_tags(registry=None, image_name=None, tags_list=tags) + self.assertEqual(ordered_tags, ["v1", "v2", "v10"]) + + @patch('registry.get_datetime_tags') + def test_tags_are_ordered_ascending_by_date_if_the_option_is_given(self, get_datetime_tags_patched): + tags = ["e61d48b", "ff24a83", "ddd514c", "f4ba381", "9d5fab2"] + get_datetime_tags_patched.return_value = [ + { + "tag": "e61d48b", + "datetime": datetime(2025, 1, 1) + }, + { + "tag": "ff24a83", + "datetime": datetime(2024, 1, 1) + }, + { + "tag": "ddd514c", + "datetime": datetime(2023, 1, 1) + }, + { + "tag": "f4ba381", + "datetime": datetime(2022, 1, 1) + }, + { + "tag": "9d5fab2", + "datetime": datetime(2021, 1, 1) + } + ] + ordered_tags = get_ordered_tags(registry="registry", image_name="image", tags_list=tags, order_by_date=True) + get_datetime_tags_patched.assert_called_once_with("registry", "image", tags) + self.assertEqual(ordered_tags, ["9d5fab2", "f4ba381", "ddd514c", "ff24a83", "e61d48b"]) + + class TestKeepImagesLike(unittest.TestCase): # tests the filtering works @@ -776,13 +845,15 @@ class TestArgParser(unittest.TestCase): "--layers", "--delete-by-hours", "24", "--keep-by-hours", "24", - "--digest-method", "GET"] + "--digest-method", "GET", + "--order-by-age"] args = parse_args(args_list) self.assertTrue(args.delete) self.assertTrue(args.layers) self.assertTrue(args.no_validate_ssl) self.assertTrue(args.delete_all) self.assertTrue(args.layers) + self.assertTrue(args.order_by_date) self.assertEqual(args.image, ["imagename1", "imagename2"]) self.assertEqual(args.num, "15") self.assertEqual(args.login, "loginstring") @@ -798,6 +869,7 @@ class TestArgParser(unittest.TestCase): "-l", "loginstring"] args = parse_args(args_list) self.assertEqual(args.digest_method, "HEAD") + self.assertFalse(args.order_by_date) if __name__ == '__main__':