7474import logging
7575from typing import Optional , Set , Dict , List
7676from datetime import datetime
77+ from concurrent .futures import ThreadPoolExecutor
7778
7879QUAY_OAUTH_TOKEN_ENV_NAME = "QUAY_OAUTH_TOKEN"
7980
@@ -134,8 +135,8 @@ def create_tag(repository: str, tag: str, manifest_digest: str, token: str):
134135 return False
135136
136137
137- def delete_tag (repository : str , tag : str , token : str ):
138- """Delete a tag from a quay.io repository"""
138+ def delete_tag (repository : str , tag : str , token : str ) -> bool :
139+ """Delete a tag from a quay.io repository. Returns True on success, False on failure. """
139140 delete_url = f"https://quay.io/api/v1/repository/{ repository } /tag/{ tag } "
140141 headers = {
141142 "Authorization" : f"Bearer { token } "
@@ -148,10 +149,14 @@ def delete_tag(repository: str, tag: str, token: str):
148149 try :
149150 with urllib .request .urlopen (request ) as response :
150151 response_data = response .read ()
151- if response .status != 204 :
152- logging .error ("Failed to delete tag '%s': %d %s" , tag , response .status , response_data )
152+ if response .status == 204 :
153+ logging .info ('Successfully deleted %s' , tag )
154+ return True
155+ logging .error ("Failed to delete tag '%s': %d %s" , tag , response .status , response_data )
156+ return False
153157 except Exception : # pylint: disable=broad-except
154158 logging .exception ('Failed to delete tag "%s"' , tag )
159+ return False
155160
156161
157162def fetch_tags (repository : str , token : str , page : int = 1 , like : Optional [str ] = None ):
@@ -318,7 +323,6 @@ def run(args, start_time): # pylint: disable=too-many-statements,redefined-oute
318323 has_more = True
319324
320325 prune_target_tags = set ()
321- pruned_tags = set ()
322326 tag_count = 0
323327 mod_by = 5
324328
@@ -328,6 +332,10 @@ def run(args, start_time): # pylint: disable=too-many-statements,redefined-oute
328332 remove_requests : Set [str ] = set () # versions to remove
329333 component_tags : Dict [str , List [str ]] = {} # version -> list of component tag names
330334
335+ # Executor for concurrent tag deletions (up to 100 simultaneous requests)
336+ delete_executor = ThreadPoolExecutor (max_workers = 100 )
337+ delete_futures = [] # List of futures
338+
331339 while has_more :
332340 retries = 5
333341 while True :
@@ -394,17 +402,21 @@ def run(args, start_time): # pylint: disable=too-many-statements,redefined-oute
394402 if days_difference > ttl_days and image_tag not in prune_target_tags :
395403 prune_target_tags .add (image_tag )
396404 if confirm :
397- try :
398- delete_tag (QUAY_CI_REPO , tag = image_tag , token = token )
399- logging .debug ('Removed %s' , image_tag )
400- pruned_tags .add (image_tag )
401- except Exception : # pylint: disable=broad-except
402- logging .exception ('Error while trying to delete tag %s' , image_tag )
405+ delete_futures .append (delete_executor .submit (delete_tag , QUAY_CI_REPO , image_tag , token ))
403406 else :
404407 logging .debug ('Would have removed %s' , image_tag )
405408
406409 page += 1
407410
411+ # Wait for all prune delete operations to complete
412+ prune_success_count = 0
413+ if delete_futures :
414+ logging .info ('Waiting for %d prune delete operations to complete...' , len (delete_futures ))
415+ for future in delete_futures :
416+ if future .result (): # Returns True on success
417+ prune_success_count += 1
418+ delete_futures .clear ()
419+
408420 # Process release payload preservation
409421 logging .info ("Processing release payload preservation..." )
410422 logging .info ("Found %d rc_payload__ tags" , len (rc_payload_tags ))
@@ -429,8 +441,8 @@ def run(args, start_time): # pylint: disable=too-many-statements,redefined-oute
429441
430442 # Process removal requests
431443 removal_requests_processed = 0
432- removal_tags_removed = 0
433444 removal_tags_target = 0
445+ removal_success_count = 0
434446
435447 if remove_requests :
436448 logging .info ("Processing %d removal requests..." , len (remove_requests ))
@@ -461,12 +473,7 @@ def run(args, start_time): # pylint: disable=too-many-statements,redefined-oute
461473 # Remove all related tags
462474 if confirm :
463475 for tag_to_remove in tags_to_remove :
464- try :
465- delete_tag (QUAY_CI_REPO , tag_to_remove , token )
466- logging .info ("Removed tag: %s" , tag_to_remove )
467- removal_tags_removed += 1
468- except Exception : # pylint: disable=broad-except
469- logging .exception ("Error removing tag %s" , tag_to_remove )
476+ delete_futures .append (delete_executor .submit (delete_tag , QUAY_CI_REPO , tag_to_remove , token ))
470477 else :
471478 for tag_to_remove in tags_to_remove :
472479 logging .info ("Would remove tag: %s" , tag_to_remove )
@@ -477,30 +484,36 @@ def run(args, start_time): # pylint: disable=too-many-statements,redefined-oute
477484 removal_tags_target += 1
478485
479486 if confirm :
480- try :
481- delete_tag (QUAY_CI_REPO , remove_tag , token )
482- logging .info ("Removed removal request tag: %s" , remove_tag )
483- removal_tags_removed += 1
484- except Exception : # pylint: disable=broad-except
485- logging .exception ("Error removing request tag %s" , remove_tag )
487+ delete_futures .append (delete_executor .submit (delete_tag , QUAY_CI_REPO , remove_tag , token ))
486488 else :
487489 logging .info ("Would remove request tag: %s" , remove_tag )
488490
491+ # Wait for all removal delete operations to complete
492+ if delete_futures :
493+ logging .info ('Waiting for %d removal delete operations to complete...' , len (delete_futures ))
494+ for future in delete_futures :
495+ if future .result (): # Returns True on success
496+ removal_success_count += 1
497+ delete_futures .clear ()
498+
489499 finish_time = datetime .now ()
490500 logging .info ('Duration: %s' , finish_time - start_time )
491501 logging .info ('Total tags scanned: %d' , tag_count )
492- logging .info ('Tags pruned (if --confirm): %d' , len (prune_target_tags ))
493- logging .info ('Tags actually pruned: %d' , len (pruned_tags ))
502+ logging .info ('Tags targeted for pruning: %d' , len (prune_target_tags ))
503+ if confirm :
504+ logging .info ('Tags successfully pruned: %d' , prune_success_count )
494505 logging .info ('Release payloads needing preservation: %d' , payloads_needing_preservation )
495506 if confirm :
496507 logging .info ('Release payloads preserved: %d' , payloads_preserved )
497508 else :
498509 logging .info ('Release payloads that would be preserved: %d' , payloads_needing_preservation )
499510 logging .info ('Removal requests processed: %d' , removal_requests_processed )
511+ logging .info ('Release payload tags targeted for removal: %d' , removal_tags_target )
500512 if confirm :
501- logging .info ('Release payload tags removed: %d' , removal_tags_removed )
502- else :
503- logging .info ('Release payload tags that would be removed: %d' , removal_tags_target )
513+ logging .info ('Release payload tags successfully removed: %d' , removal_success_count )
514+
515+ # Cleanup executor
516+ delete_executor .shutdown (wait = False )
504517
505518logging .basicConfig (level = logging .DEBUG , format = '%(asctime)s - %(levelname)s - %(message)s' , datefmt = '%Y-%m-%dT%H:%M:%S' )
506519
0 commit comments