3434
3535import argparse
3636import json
37+ import subprocess
3738from urllib .parse import urlparse
3839import pytz
40+ import requests
3941import yaml
4042import collections
4143import datetime
@@ -59,7 +61,8 @@ CACHE_MAX_AGE_METADATA = 60 * 5
5961# is up to date.
6062SUPPORTED = ["amis" , "gcp" ]
6163UNSUPPORTED = ["aliyun" , "azure" , "ibmcloud" , "powervs" ]
62-
64+ # list of known streams with containers
65+ STREAMS = {"next" , "testing" , "stable" , "next-devel" , "testing-devel" , "rawhide" , "branched" }
6366
6467def parse_args ():
6568 parser = argparse .ArgumentParser (prog = "coreos-assembler cloud-prune" )
@@ -70,6 +73,9 @@ def parse_args():
7073 parser .add_argument ("--gcp-json-key" , help = "GCP Service Account JSON Auth" , default = os .environ .get ("GCP_JSON_AUTH" ))
7174 parser .add_argument ("--acl" , help = "ACL for objects" , action = 'store' , default = 'private' )
7275 parser .add_argument ("--aws-config-file" , default = os .environ .get ("AWS_CONFIG_FILE" ), help = "Path to AWS config file" )
76+ parser .add_argument ("--repository-url" , help = "container images URL" )
77+ parser .add_argument ("--registry-auth-file" , default = os .environ .get ("REGISTRY_AUTH_FILE" ),
78+ help = "Path to docker registry auth file. Directly passed to skopeo." )
7379 return parser .parse_args ()
7480
7581
@@ -125,7 +131,7 @@ def main():
125131 current_build = Build (id = build_id , images = images , arch = arch , meta_json = meta_json )
126132
127133 # Iterate over actions (policy types) to apply pruning
128- for action in ['cloud-uploads' , 'images' , 'build' ]:
134+ for action in ['cloud-uploads' , 'images' , 'build' , 'containers' ]:
129135 if action not in policy [stream ]:
130136 continue
131137 action_duration = convert_duration_to_days (policy [stream ][action ])
@@ -162,6 +168,19 @@ def main():
162168 case "build" :
163169 prune_build (s3_client , bucket , prefix , build_id , args .dry_run )
164170 pruned_build_ids .append (build_id )
171+ case "containers" :
172+ container_tags = get_container_tags (meta_json , stream )
173+ if container_tags :
174+ containers_config = {
175+ "container_tags" : container_tags ,
176+ "dry_run" : args .dry_run ,
177+ "repository_url" : args .repository_url ,
178+ "registry_auth_file" : args .registry_auth_file ,
179+ "stream" : stream
180+ }
181+ prune_containers (containers_config )
182+ else :
183+ print (f"No container tags to prune for build { build_id } on architecture { arch } ." )
165184
166185 # Update policy-cleanup after pruning actions for the architecture
167186 policy_cleanup = build .setdefault ("policy-cleanup" , {})
@@ -174,6 +193,9 @@ def main():
174193 if "images" not in policy_cleanup :
175194 policy_cleanup ["images" ] = True
176195 policy_cleanup ["images-kept" ] = images_to_keep
196+ case "containers" :
197+ if "containers" not in policy_cleanup :
198+ policy_cleanup ["containers" ] = True
177199
178200 if pruned_build_ids :
179201 if "tombstone-builds" not in builds_json_data :
@@ -414,5 +436,52 @@ def prune_build(s3_client, bucket, prefix, build_id, dry_run):
414436 raise Exception (f"Error pruning { build_id } : { e .response ['Error' ]['Message' ]} " )
415437
416438
439+ def get_container_tags (meta_json , stream ):
440+ container_tags = []
441+ base_oscontainer = meta_json .get ("base-oscontainer" )
442+ if base_oscontainer :
443+ tags = base_oscontainer .get ("tags" , [])
444+ # Only include tags that do not match the stream i.e. moving tags
445+ filtered_tags = [tag for tag in tags if tag != stream ]
446+ if filtered_tags :
447+ container_tags = filtered_tags
448+ return container_tags
449+
450+
451+ def prune_containers (containers_config ):
452+ barrier_releases = set ()
453+ # Get the update graph for stable streams
454+ if containers_config .stream in ['stable' , 'testing' , 'next' ]:
455+ update_graph = get_update_graph (containers_config .stream )['releases' ]
456+ # Keep only the barrier releases
457+ barrier_releases = set ([release ["version" ] for release in update_graph if "barrier" in release ])
458+
459+ for tag in containers_config .container_tags :
460+ if tag in STREAMS :
461+ continue
462+ if tag in barrier_releases :
463+ print (f"Release { tag } is a barrier release, keeping." )
464+ continue
465+ if containers_config .dry_run :
466+ print (f"Would prune image { containers_config .repository_url } :{ tag } " )
467+ else :
468+ skopeo_delete (containers_config .repository_url , tag , containers_config .registry_auth_file )
469+
470+
471+ def get_update_graph (stream ):
472+ url = f"https://builds.coreos.fedoraproject.org/updates/{ stream } .json"
473+ r = requests .get (url , timeout = 5 )
474+ if r .status_code != 200 :
475+ raise Exception (f"Could not download update graph for { stream } . HTTP { r .status_code } " )
476+ return r .json ()
477+
478+
479+ def skopeo_delete (repo , image , auth ):
480+ skopeo_args = ["skopeo" , "delete" , f"docker://{ repo } :{ image } " ]
481+ if auth is not None :
482+ skopeo_args .append (f"--authfile { auth } " )
483+ subprocess .check_output (skopeo_args )
484+
485+
417486if __name__ == "__main__" :
418487 main ()
0 commit comments