Skip to content

Commit b300437

Browse files
committed
Build.cleanup_builds is now a standalone traitlets-based class
1 parent 470b9ea commit b300437

File tree

1 file changed

+95
-60
lines changed

1 file changed

+95
-60
lines changed

binderhub/build.py

Lines changed: 95 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
628648
class 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

Comments
 (0)