@@ -274,66 +274,6 @@ def _default_api(self):
274274
275275 _component_label = Unicode ("binderhub-build" )
276276
277- @classmethod
278- def cleanup_builds (cls , kube , namespace , max_age ):
279- """Delete stopped build pods and build pods that have aged out"""
280- builds = kube .list_namespaced_pod (
281- namespace = namespace ,
282- label_selector = "component=binderhub-build" ,
283- ).items
284- phases = defaultdict (int )
285- app_log .debug ("%i build pods" , len (builds ))
286- now = datetime .datetime .now (tz = datetime .timezone .utc )
287- start_cutoff = now - datetime .timedelta (seconds = max_age )
288- deleted = 0
289- for build in builds :
290- phase = build .status .phase
291- phases [phase ] += 1
292- annotations = build .metadata .annotations or {}
293- repo = annotations .get ("binder-repo" , "unknown" )
294- delete = False
295- if build .status .phase in {"Failed" , "Succeeded" , "Evicted" }:
296- # log Deleting Failed build build-image-...
297- # print(build.metadata)
298- app_log .info (
299- "Deleting %s build %s (repo=%s)" ,
300- build .status .phase ,
301- build .metadata .name ,
302- repo ,
303- )
304- delete = True
305- else :
306- # check age
307- started = build .status .start_time
308- if max_age and started and started < start_cutoff :
309- app_log .info (
310- "Deleting long-running build %s (repo=%s)" ,
311- build .metadata .name ,
312- repo ,
313- )
314- delete = True
315-
316- if delete :
317- deleted += 1
318- try :
319- kube .delete_namespaced_pod (
320- name = build .metadata .name ,
321- namespace = namespace ,
322- body = client .V1DeleteOptions (grace_period_seconds = 0 ),
323- )
324- except client .rest .ApiException as e :
325- if e .status == 404 :
326- # Is ok, someone else has already deleted it
327- pass
328- else :
329- raise
330-
331- if deleted :
332- app_log .info ("Deleted %i/%i build pods" , deleted , len (builds ))
333- app_log .debug (
334- "Build phase summary: %s" , json .dumps (phases , sort_keys = True , indent = 1 )
335- )
336-
337277 def get_affinity (self ):
338278 """Determine the affinity term for the build pod.
339279
@@ -625,6 +565,86 @@ def cleanup(self):
625565 raise
626566
627567
568+ class KubernetesCleaner (LoggingConfigurable ):
569+ """Regular cleanup utility for kubernetes builds
570+
571+ Instantiate this class, and call cleanup() periodically.
572+ """
573+
574+ kube = Any (help = "kubernetes API client" )
575+
576+ @default ("kube" )
577+ def _default_kube (self ):
578+ try :
579+ kubernetes .config .load_incluster_config ()
580+ except kubernetes .config .ConfigException :
581+ kubernetes .config .load_kube_config ()
582+ return client .CoreV1Api ()
583+
584+ namespace = Unicode (help = "Kubernetes namespace" , config = True )
585+
586+ max_age = Integer (help = "Maximum age of build pods to keep" , config = True )
587+
588+ def cleanup (self ):
589+ """Delete stopped build pods and build pods that have aged out"""
590+ builds = self .kube .list_namespaced_pod (
591+ namespace = self .namespace ,
592+ label_selector = "component=binderhub-build" ,
593+ ).items
594+ phases = defaultdict (int )
595+ app_log .debug ("%i build pods" , len (builds ))
596+ now = datetime .datetime .now (tz = datetime .timezone .utc )
597+ start_cutoff = now - datetime .timedelta (seconds = self .max_age )
598+ deleted = 0
599+ for build in builds :
600+ phase = build .status .phase
601+ phases [phase ] += 1
602+ annotations = build .metadata .annotations or {}
603+ repo = annotations .get ("binder-repo" , "unknown" )
604+ delete = False
605+ if build .status .phase in {"Failed" , "Succeeded" , "Evicted" }:
606+ # log Deleting Failed build build-image-...
607+ # print(build.metadata)
608+ app_log .info (
609+ "Deleting %s build %s (repo=%s)" ,
610+ build .status .phase ,
611+ build .metadata .name ,
612+ repo ,
613+ )
614+ delete = True
615+ else :
616+ # check age
617+ started = build .status .start_time
618+ if self .max_age and started and started < start_cutoff :
619+ app_log .info (
620+ "Deleting long-running build %s (repo=%s)" ,
621+ build .metadata .name ,
622+ repo ,
623+ )
624+ delete = True
625+
626+ if delete :
627+ deleted += 1
628+ try :
629+ self .kube .delete_namespaced_pod (
630+ name = build .metadata .name ,
631+ namespace = self .namespace ,
632+ body = client .V1DeleteOptions (grace_period_seconds = 0 ),
633+ )
634+ except client .rest .ApiException as e :
635+ if e .status == 404 :
636+ # Is ok, someone else has already deleted it
637+ pass
638+ else :
639+ raise
640+
641+ if deleted :
642+ app_log .info ("Deleted %i/%i build pods" , deleted , len (builds ))
643+ app_log .debug (
644+ "Build phase summary: %s" , json .dumps (phases , sort_keys = True , indent = 1 )
645+ )
646+
647+
628648class FakeBuild (BuildExecutor ):
629649 """
630650 Fake Building process to be able to work on the UI without a builder.
@@ -705,6 +725,11 @@ class Build(KubernetesBuildExecutor):
705725
706726 """
707727
728+ """
729+ For backwards compatibility: BinderHub previously assumed a single cleaner for all builds
730+ """
731+ _cleaners = {}
732+
708733 def __init__ (
709734 self ,
710735 q ,
@@ -750,3 +775,13 @@ def __init__(
750775 self .log_tail_lines = log_tail_lines
751776 self .git_credentials = git_credentials
752777 self .sticky_builds = sticky_builds
778+
779+ @classmethod
780+ def cleanup_builds (cls , kube , namespace , max_age ):
781+ """Delete stopped build pods and build pods that have aged out"""
782+ key = (kube , namespace , max_age )
783+ if key not in Build ._cleaners :
784+ Build ._cleaners [key ] = KubernetesCleaner (
785+ kube = kube , namespace = namespace , max_age = max_age
786+ )
787+ Build ._cleaners [key ].cleanup ()
0 commit comments