cleanup(jenkins): detect longhorn orphan volumes via kubernetes status
This commit is contained in:
parent
4cc2f0c355
commit
27788d307f
@ -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
|
||||
|
||||
@ -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))
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user