2026-04-12 04:49:25 -03:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
import types
|
|
|
|
|
|
2026-04-12 12:28:50 -03:00
|
|
|
from prometheus_client import REGISTRY
|
|
|
|
|
|
2026-04-12 04:49:25 -03:00
|
|
|
from ariadne.services import jenkins_workspace_cleanup as cleanup_module
|
|
|
|
|
|
|
|
|
|
|
2026-04-12 12:28:50 -03:00
|
|
|
def _metric_value(name: str, labels: dict[str, str]) -> float:
|
|
|
|
|
value = REGISTRY.get_sample_value(name, labels)
|
|
|
|
|
return float(value) if value is not None else 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _dummy_settings(*, dry_run: bool, max_deletions: int = 20) -> types.SimpleNamespace:
|
|
|
|
|
return types.SimpleNamespace(
|
2026-04-12 04:49:25 -03:00
|
|
|
jenkins_workspace_namespace="jenkins",
|
|
|
|
|
jenkins_workspace_pvc_prefix="pvc-workspace-",
|
|
|
|
|
jenkins_workspace_cleanup_min_age_hours=1.0,
|
2026-04-12 12:28:50 -03:00
|
|
|
jenkins_workspace_cleanup_dry_run=dry_run,
|
|
|
|
|
jenkins_workspace_cleanup_max_deletions_per_run=max_deletions,
|
2026-04-12 04:49:25 -03:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-04-12 12:28:50 -03:00
|
|
|
def _fake_payloads(now_iso: str, old_iso: str) -> dict[str, dict[str, object]]:
|
|
|
|
|
return {
|
|
|
|
|
"/api/v1/namespaces/jenkins/pods": {
|
|
|
|
|
"items": [
|
|
|
|
|
{
|
|
|
|
|
"metadata": {
|
|
|
|
|
"annotations": {
|
|
|
|
|
"jenkins.io/workspace-pvc": "pvc-workspace-annotated-active",
|
2026-04-12 04:49:25 -03:00
|
|
|
}
|
|
|
|
|
},
|
2026-04-12 12:28:50 -03:00
|
|
|
"spec": {
|
|
|
|
|
"volumes": [
|
|
|
|
|
{"persistentVolumeClaim": {"claimName": "pvc-workspace-active"}},
|
|
|
|
|
]
|
2026-04-12 04:49:25 -03:00
|
|
|
},
|
2026-04-12 12:28:50 -03:00
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
"/api/v1/namespaces/jenkins/persistentvolumeclaims": {
|
|
|
|
|
"items": [
|
|
|
|
|
{
|
|
|
|
|
"metadata": {"name": "pvc-workspace-stale", "creationTimestamp": old_iso},
|
|
|
|
|
"status": {"phase": "Lost"},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"metadata": {"name": "pvc-workspace-active", "creationTimestamp": old_iso},
|
|
|
|
|
"status": {"phase": "Bound"},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"metadata": {"name": "pvc-workspace-annotated-active", "creationTimestamp": old_iso},
|
|
|
|
|
"status": {"phase": "Lost"},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"metadata": {"name": "pvc-workspace-fresh", "creationTimestamp": now_iso},
|
|
|
|
|
"status": {"phase": "Lost"},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"metadata": {
|
|
|
|
|
"name": "pvc-workspace-deleting",
|
|
|
|
|
"creationTimestamp": old_iso,
|
|
|
|
|
"deletionTimestamp": old_iso,
|
2026-04-12 04:49:25 -03:00
|
|
|
},
|
2026-04-12 12:28:50 -03:00
|
|
|
"status": {"phase": "Lost"},
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
"/api/v1/persistentvolumes": {
|
|
|
|
|
"items": [
|
|
|
|
|
{
|
|
|
|
|
"metadata": {"name": "pvc-old", "creationTimestamp": old_iso},
|
|
|
|
|
"status": {"phase": "Released"},
|
|
|
|
|
"spec": {"claimRef": {"namespace": "jenkins", "name": "pvc-workspace-stale"}},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"metadata": {"name": "pvc-active", "creationTimestamp": old_iso},
|
|
|
|
|
"status": {"phase": "Released"},
|
|
|
|
|
"spec": {"claimRef": {"namespace": "jenkins", "name": "pvc-workspace-active"}},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"metadata": {"name": "pvc-annotated", "creationTimestamp": old_iso},
|
|
|
|
|
"status": {"phase": "Released"},
|
|
|
|
|
"spec": {"claimRef": {"namespace": "jenkins", "name": "pvc-workspace-annotated-active"}},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"metadata": {"name": "pvc-fresh", "creationTimestamp": now_iso},
|
|
|
|
|
"status": {"phase": "Released"},
|
|
|
|
|
"spec": {"claimRef": {"namespace": "jenkins", "name": "pvc-workspace-fresh"}},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"metadata": {
|
|
|
|
|
"name": "pvc-deleting",
|
|
|
|
|
"creationTimestamp": old_iso,
|
|
|
|
|
"deletionTimestamp": old_iso,
|
2026-04-12 04:49:25 -03:00
|
|
|
},
|
2026-04-12 12:28:50 -03:00
|
|
|
"status": {"phase": "Released"},
|
|
|
|
|
"spec": {"claimRef": {"namespace": "jenkins", "name": "pvc-workspace-deleting"}},
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
"/apis/longhorn.io/v1beta2/namespaces/longhorn-system/volumes": {
|
|
|
|
|
"items": [
|
|
|
|
|
{"metadata": {"name": "pvc-old", "creationTimestamp": old_iso}},
|
|
|
|
|
{
|
|
|
|
|
"metadata": {
|
|
|
|
|
"name": "pvc-orphan",
|
|
|
|
|
"creationTimestamp": old_iso,
|
|
|
|
|
"labels": {
|
|
|
|
|
"kubernetes.io/created-for/pvc/name": "pvc-workspace-orphan",
|
2026-04-12 14:24:17 -03:00
|
|
|
"kubernetes.io/created-for/pvc/namespace": "jenkins",
|
2026-04-12 12:28:50 -03:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"metadata": {
|
|
|
|
|
"name": "pvc-attached",
|
|
|
|
|
"creationTimestamp": old_iso,
|
|
|
|
|
"labels": {
|
|
|
|
|
"kubernetes.io/created-for/pvc/name": "pvc-workspace-annotated-active",
|
2026-04-12 14:24:17 -03:00
|
|
|
"kubernetes.io/created-for/pvc/namespace": "jenkins",
|
2026-04-12 12:28:50 -03:00
|
|
|
},
|
2026-04-12 04:49:25 -03:00
|
|
|
},
|
2026-04-12 12:28:50 -03:00
|
|
|
"status": {"state": "attached", "isAttached": True, "robustness": "healthy"},
|
|
|
|
|
"spec": {"frontend": "blockdev"},
|
|
|
|
|
},
|
2026-04-12 14:24:17 -03:00
|
|
|
{
|
|
|
|
|
"metadata": {
|
|
|
|
|
"name": "pvc-orphan-other-namespace",
|
|
|
|
|
"creationTimestamp": old_iso,
|
|
|
|
|
"labels": {
|
|
|
|
|
"kubernetes.io/created-for/pvc/name": "pvc-workspace-orphan",
|
|
|
|
|
"kubernetes.io/created-for/pvc/namespace": "nextcloud",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-04-12 12:28:50 -03:00
|
|
|
{
|
|
|
|
|
"metadata": {
|
|
|
|
|
"name": "pvc-orphan-fresh",
|
|
|
|
|
"creationTimestamp": now_iso,
|
|
|
|
|
"labels": {
|
|
|
|
|
"kubernetes.io/created-for/pvc/name": "pvc-workspace-fresh",
|
2026-04-12 14:24:17 -03:00
|
|
|
"kubernetes.io/created-for/pvc/namespace": "jenkins",
|
2026-04-12 12:28:50 -03:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"metadata": {
|
|
|
|
|
"name": "pvc-vol-deleting",
|
|
|
|
|
"creationTimestamp": old_iso,
|
|
|
|
|
"deletionTimestamp": old_iso,
|
|
|
|
|
"labels": {
|
|
|
|
|
"kubernetes.io/created-for/pvc/name": "pvc-workspace-orphan",
|
2026-04-12 14:24:17 -03:00
|
|
|
"kubernetes.io/created-for/pvc/namespace": "jenkins",
|
2026-04-12 12:28:50 -03:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cleanup_jenkins_workspace_storage_dry_run(monkeypatch) -> None:
|
|
|
|
|
monkeypatch.setattr(cleanup_module, "settings", _dummy_settings(dry_run=True))
|
|
|
|
|
|
|
|
|
|
now_iso = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
|
|
|
old_iso = "2020-01-01T00:00:00Z"
|
|
|
|
|
payloads = _fake_payloads(now_iso, old_iso)
|
|
|
|
|
deleted_paths: list[str] = []
|
|
|
|
|
|
|
|
|
|
def fake_get_json(path: str):
|
|
|
|
|
if path in payloads:
|
|
|
|
|
return payloads[path]
|
2026-04-12 04:49:25 -03:00
|
|
|
raise AssertionError(f"unexpected path: {path}")
|
|
|
|
|
|
|
|
|
|
def fake_delete_json(path: str):
|
|
|
|
|
deleted_paths.append(path)
|
|
|
|
|
return {"status": "Success"}
|
|
|
|
|
|
2026-04-12 12:28:50 -03:00
|
|
|
before_runs = _metric_value(
|
|
|
|
|
"ariadne_jenkins_workspace_cleanup_runs_total",
|
|
|
|
|
{"status": "ok", "mode": "dry_run"},
|
|
|
|
|
)
|
|
|
|
|
before_planned = _metric_value(
|
|
|
|
|
"ariadne_jenkins_workspace_cleanup_objects_total",
|
|
|
|
|
{"kind": "pvc", "action": "planned", "mode": "dry_run"},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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.dry_run is True
|
|
|
|
|
assert summary.pvcs_planned == 1
|
|
|
|
|
assert summary.pvs_planned == 1
|
|
|
|
|
assert summary.volumes_planned == 2
|
|
|
|
|
assert summary.pvcs_deleted == 0
|
|
|
|
|
assert summary.pvs_deleted == 0
|
|
|
|
|
assert summary.volumes_deleted == 0
|
|
|
|
|
assert summary.failures == 0
|
|
|
|
|
assert deleted_paths == []
|
|
|
|
|
assert _metric_value(
|
|
|
|
|
"ariadne_jenkins_workspace_cleanup_runs_total",
|
|
|
|
|
{"status": "ok", "mode": "dry_run"},
|
|
|
|
|
) == before_runs + 1
|
|
|
|
|
assert _metric_value(
|
|
|
|
|
"ariadne_jenkins_workspace_cleanup_objects_total",
|
|
|
|
|
{"kind": "pvc", "action": "planned", "mode": "dry_run"},
|
|
|
|
|
) == before_planned + 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cleanup_jenkins_workspace_storage(monkeypatch) -> None:
|
|
|
|
|
monkeypatch.setattr(cleanup_module, "settings", _dummy_settings(dry_run=False))
|
|
|
|
|
|
|
|
|
|
now_iso = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
|
|
|
old_iso = "2020-01-01T00:00:00Z"
|
|
|
|
|
deleted_paths: list[str] = []
|
|
|
|
|
payloads = _fake_payloads(now_iso, old_iso)
|
|
|
|
|
|
|
|
|
|
def fake_get_json(path: str):
|
|
|
|
|
if path in payloads:
|
|
|
|
|
return payloads[path]
|
|
|
|
|
raise AssertionError(f"unexpected path: {path}")
|
|
|
|
|
|
|
|
|
|
def fake_delete_json(path: str):
|
|
|
|
|
deleted_paths.append(path)
|
|
|
|
|
return {"status": "Success"}
|
|
|
|
|
|
|
|
|
|
before_runs = _metric_value(
|
|
|
|
|
"ariadne_jenkins_workspace_cleanup_runs_total",
|
|
|
|
|
{"status": "ok", "mode": "delete"},
|
|
|
|
|
)
|
|
|
|
|
before_deleted = _metric_value(
|
|
|
|
|
"ariadne_jenkins_workspace_cleanup_objects_total",
|
|
|
|
|
{"kind": "longhorn_volume", "action": "deleted", "mode": "delete"},
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-12 04:49:25 -03:00
|
|
|
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.pvcs_deleted == 1
|
|
|
|
|
assert summary.pvs_deleted == 1
|
|
|
|
|
assert summary.volumes_deleted == 2
|
|
|
|
|
assert summary.failures == 0
|
|
|
|
|
assert "/api/v1/namespaces/jenkins/persistentvolumeclaims/pvc-workspace-stale" in deleted_paths
|
|
|
|
|
assert "/api/v1/persistentvolumes/pvc-old" in deleted_paths
|
|
|
|
|
assert "/apis/longhorn.io/v1beta2/namespaces/longhorn-system/volumes/pvc-old" in deleted_paths
|
|
|
|
|
assert "/apis/longhorn.io/v1beta2/namespaces/longhorn-system/volumes/pvc-orphan" in deleted_paths
|
2026-04-12 14:24:17 -03:00
|
|
|
assert "/apis/longhorn.io/v1beta2/namespaces/longhorn-system/volumes/pvc-orphan-other-namespace" not in deleted_paths
|
2026-04-12 12:28:50 -03:00
|
|
|
assert "/apis/longhorn.io/v1beta2/namespaces/longhorn-system/volumes/pvc-attached" not in deleted_paths
|
|
|
|
|
assert _metric_value(
|
|
|
|
|
"ariadne_jenkins_workspace_cleanup_runs_total",
|
|
|
|
|
{"status": "ok", "mode": "delete"},
|
|
|
|
|
) == before_runs + 1
|
|
|
|
|
assert _metric_value(
|
|
|
|
|
"ariadne_jenkins_workspace_cleanup_objects_total",
|
|
|
|
|
{"kind": "longhorn_volume", "action": "deleted", "mode": "delete"},
|
|
|
|
|
) == before_deleted + 2
|
2026-04-12 04:49:25 -03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cleanup_jenkins_workspace_storage_failure(monkeypatch) -> None:
|
2026-04-12 12:28:50 -03:00
|
|
|
monkeypatch.setattr(cleanup_module, "settings", _dummy_settings(dry_run=False))
|
2026-04-12 04:49:25 -03:00
|
|
|
|
|
|
|
|
def fake_get_json(path: str):
|
|
|
|
|
if path == "/api/v1/namespaces/jenkins/pods":
|
|
|
|
|
return {"items": []}
|
|
|
|
|
if path == "/api/v1/namespaces/jenkins/persistentvolumeclaims":
|
|
|
|
|
return {
|
|
|
|
|
"items": [
|
|
|
|
|
{
|
|
|
|
|
"metadata": {"name": "pvc-workspace-stale", "creationTimestamp": "2020-01-01T00:00:00Z"},
|
|
|
|
|
"status": {"phase": "Lost"},
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
if path == "/api/v1/persistentvolumes":
|
|
|
|
|
return {"items": []}
|
|
|
|
|
if path == "/apis/longhorn.io/v1beta2/namespaces/longhorn-system/volumes":
|
|
|
|
|
return {"items": []}
|
|
|
|
|
raise AssertionError(f"unexpected path: {path}")
|
|
|
|
|
|
|
|
|
|
def fake_delete_json(_path: str):
|
|
|
|
|
raise RuntimeError("boom")
|
|
|
|
|
|
2026-04-12 12:28:50 -03:00
|
|
|
before_failures = _metric_value(
|
|
|
|
|
"ariadne_jenkins_workspace_cleanup_objects_total",
|
|
|
|
|
{"kind": "cleanup", "action": "failed", "mode": "delete"},
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-12 04:49:25 -03:00
|
|
|
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.failures == 1
|
|
|
|
|
assert summary.pvcs_deleted == 0
|
2026-04-12 12:28:50 -03:00
|
|
|
assert _metric_value(
|
|
|
|
|
"ariadne_jenkins_workspace_cleanup_objects_total",
|
|
|
|
|
{"kind": "cleanup", "action": "failed", "mode": "delete"},
|
|
|
|
|
) == before_failures + 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
|
|
|
|
|
now_iso = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
|
|
|
old_iso = "2020-01-01T00:00:00Z"
|
|
|
|
|
payloads = _fake_payloads(now_iso, old_iso)
|
|
|
|
|
deleted_paths: list[str] = []
|
|
|
|
|
|
|
|
|
|
def fake_get_json(path: str):
|
|
|
|
|
if path in payloads:
|
|
|
|
|
return payloads[path]
|
|
|
|
|
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.failures == 0
|
|
|
|
|
assert summary.pvcs_planned == 1
|
|
|
|
|
assert summary.pvs_planned == 1
|
|
|
|
|
assert summary.volumes_planned == 1
|
|
|
|
|
assert summary.pvcs_deleted == 1
|
|
|
|
|
assert summary.pvs_deleted == 0
|
|
|
|
|
assert summary.volumes_deleted == 0
|
|
|
|
|
assert summary.skipped == 2
|
|
|
|
|
assert deleted_paths == ["/api/v1/namespaces/jenkins/persistentvolumeclaims/pvc-workspace-stale"]
|