cleanup(jenkins): detect longhorn orphan volumes via kubernetes status

This commit is contained in:
Brad Stein 2026-04-12 14:28:08 -03:00
parent 4cc2f0c355
commit 27788d307f
2 changed files with 104 additions and 15 deletions

View File

@ -93,6 +93,13 @@ class _CleanupCandidate:
pv_name: str | None = None
@dataclass(frozen=True)
class _LonghornBinding:
pvc_name: Any
pvc_namespace: Any
referenced_pv_name: Any
def _parse_timestamp(raw: str) -> datetime | None:
"""Parse Kubernetes RFC3339 timestamps into timezone-aware datetimes."""
@ -245,6 +252,47 @@ def _workspace_pvc_candidates(active_claims: set[str]) -> list[_CleanupCandidate
return candidates
def _workspace_binding_from_longhorn(
metadata: dict[str, Any],
status: dict[str, Any],
) -> _LonghornBinding:
labels = metadata.get("labels") if isinstance(metadata.get("labels"), dict) else {}
kubernetes_status = status.get("kubernetesStatus") if isinstance(status.get("kubernetesStatus"), dict) else {}
pvc_name = labels.get("kubernetes.io/created-for/pvc/name")
if not isinstance(pvc_name, str) or not pvc_name:
pvc_name = kubernetes_status.get("pvcName")
pvc_namespace = labels.get("kubernetes.io/created-for/pvc/namespace")
if not isinstance(pvc_namespace, str) or not pvc_namespace:
pvc_namespace = kubernetes_status.get("namespace")
referenced_pv_name = kubernetes_status.get("pvName")
return _LonghornBinding(
pvc_name=pvc_name,
pvc_namespace=pvc_namespace,
referenced_pv_name=referenced_pv_name,
)
def _should_delete_longhorn_volume(
name: str,
binding: _LonghornBinding,
all_pv_names: set[str],
removed_pv_names: set[str],
) -> bool:
if name in removed_pv_names or binding.referenced_pv_name in removed_pv_names:
return True
if not _is_workspace_name(binding.pvc_name):
return False
if (
isinstance(binding.referenced_pv_name, str)
and binding.referenced_pv_name in all_pv_names
) or name in all_pv_names:
return False
return (
binding.pvc_namespace in {None, ""}
or binding.pvc_namespace == settings.jenkins_workspace_namespace
)
def _workspace_longhorn_candidates(all_pv_names: set[str], removed_pv_names: set[str]) -> list[_CleanupCandidate]:
namespace = "longhorn-system"
payload = get_json("/apis/longhorn.io/v1beta2/namespaces/longhorn-system/volumes")
@ -261,26 +309,17 @@ def _workspace_longhorn_candidates(all_pv_names: set[str], removed_pv_names: set
if not isinstance(name, str) or not name:
continue
labels = metadata.get("labels") if isinstance(metadata.get("labels"), dict) else {}
pvc_name = labels.get("kubernetes.io/created-for/pvc/name")
pvc_namespace = labels.get("kubernetes.io/created-for/pvc/namespace")
binding = _workspace_binding_from_longhorn(metadata, status)
robust_state = status.get("robustness")
state = status.get("state")
attached = status.get("isAttached")
frontend = spec.get("frontend")
should_delete = False
if name in removed_pv_names:
should_delete = True
elif (
_is_workspace_name(pvc_name)
and name not in all_pv_names
and (
pvc_namespace in {None, ""}
or pvc_namespace == settings.jenkins_workspace_namespace
)
if not _should_delete_longhorn_volume(
name,
binding,
all_pv_names,
removed_pv_names,
):
should_delete = True
if not should_delete:
continue
if _is_deleting(metadata):
continue

View File

@ -305,6 +305,56 @@ def test_cleanup_jenkins_workspace_storage_failure(monkeypatch) -> None:
) == before_failures + 1
def test_cleanup_jenkins_workspace_storage_uses_longhorn_kubernetes_status(monkeypatch) -> None:
monkeypatch.setattr(cleanup_module, "settings", _dummy_settings(dry_run=False))
deleted_paths: list[str] = []
def fake_get_json(path: str):
if path == "/api/v1/namespaces/jenkins/pods":
return {"items": []}
if path == "/api/v1/namespaces/jenkins/persistentvolumeclaims":
return {"items": []}
if path == "/api/v1/persistentvolumes":
return {"items": []}
if path == "/apis/longhorn.io/v1beta2/namespaces/longhorn-system/volumes":
return {
"items": [
{
"metadata": {
"name": "pvc-orphan-kstatus",
"creationTimestamp": "2020-01-01T00:00:00Z",
},
"status": {
"state": "detached",
"isAttached": False,
"robustness": "unknown",
"kubernetesStatus": {
"namespace": "jenkins",
"pvcName": "pvc-workspace-kstatus",
"pvName": "pvc-orphan-kstatus",
},
},
"spec": {"frontend": "blockdev"},
}
]
}
raise AssertionError(f"unexpected path: {path}")
def fake_delete_json(path: str):
deleted_paths.append(path)
return {"status": "Success"}
monkeypatch.setattr(cleanup_module, "get_json", fake_get_json)
monkeypatch.setattr(cleanup_module, "delete_json", fake_delete_json)
summary = cleanup_module.cleanup_jenkins_workspace_storage()
assert summary.volumes_planned == 1
assert summary.volumes_deleted == 1
assert summary.failures == 0
assert deleted_paths == ["/apis/longhorn.io/v1beta2/namespaces/longhorn-system/volumes/pvc-orphan-kstatus"]
def test_cleanup_jenkins_workspace_storage_guard_caps_mass_delete(monkeypatch) -> None:
monkeypatch.setattr(cleanup_module, "settings", _dummy_settings(dry_run=False, max_deletions=1))