2026-01-26 03:31:42 -03:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
|
|
|
|
|
import ariadne.services.cluster_state as cluster_state
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DummyStorage:
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
self.snapshot = None
|
|
|
|
|
self.keep = None
|
|
|
|
|
|
|
|
|
|
def record_cluster_state(self, snapshot): # type: ignore[no-untyped-def]
|
|
|
|
|
self.snapshot = snapshot
|
|
|
|
|
|
|
|
|
|
def prune_cluster_state(self, keep: int) -> None:
|
|
|
|
|
self.keep = keep
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_collect_cluster_state(monkeypatch) -> None:
|
|
|
|
|
def fake_get_json(path: str):
|
|
|
|
|
if path.endswith("/nodes"):
|
|
|
|
|
return {
|
|
|
|
|
"items": [
|
|
|
|
|
{
|
2026-01-27 05:41:46 -03:00
|
|
|
"metadata": {"name": "node-a", "labels": {"kubernetes.io/arch": "arm64"}},
|
|
|
|
|
"status": {
|
|
|
|
|
"conditions": [{"type": "Ready", "status": "True"}],
|
|
|
|
|
"nodeInfo": {"architecture": "arm64"},
|
|
|
|
|
"addresses": [{"type": "InternalIP", "address": "10.0.0.1"}],
|
|
|
|
|
},
|
2026-01-26 03:31:42 -03:00
|
|
|
},
|
|
|
|
|
{
|
2026-01-29 03:02:39 -03:00
|
|
|
"metadata": {
|
|
|
|
|
"name": "node-b",
|
|
|
|
|
"labels": {"kubernetes.io/arch": "amd64"},
|
|
|
|
|
"creationTimestamp": "2026-01-01T00:00:00Z",
|
|
|
|
|
},
|
2026-01-29 03:08:57 -03:00
|
|
|
"spec": {
|
|
|
|
|
"taints": [
|
|
|
|
|
{"key": "node-role.kubernetes.io/control-plane", "effect": "NoSchedule"}
|
|
|
|
|
]
|
|
|
|
|
},
|
2026-01-27 05:41:46 -03:00
|
|
|
"status": {
|
|
|
|
|
"conditions": [{"type": "Ready", "status": "False"}],
|
|
|
|
|
"nodeInfo": {"architecture": "amd64"},
|
|
|
|
|
},
|
2026-01-26 03:31:42 -03:00
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
}
|
2026-01-27 05:41:46 -03:00
|
|
|
if path.startswith("/api/v1/pods"):
|
|
|
|
|
return {
|
|
|
|
|
"items": [
|
|
|
|
|
{
|
|
|
|
|
"metadata": {
|
|
|
|
|
"name": "jellyfin-0",
|
|
|
|
|
"namespace": "media",
|
|
|
|
|
"labels": {"app": "jellyfin"},
|
|
|
|
|
},
|
|
|
|
|
"spec": {"nodeName": "node-a"},
|
|
|
|
|
"status": {"phase": "Running"},
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
2026-01-29 02:31:30 -03:00
|
|
|
if path.startswith("/api/v1/events"):
|
|
|
|
|
return {"items": []}
|
2026-01-29 02:39:08 -03:00
|
|
|
if path.startswith("/apis/apps/v1/deployments"):
|
|
|
|
|
return {
|
|
|
|
|
"items": [
|
|
|
|
|
{
|
|
|
|
|
"metadata": {"name": "api", "namespace": "apps"},
|
|
|
|
|
"spec": {"replicas": 2},
|
|
|
|
|
"status": {"readyReplicas": 1, "availableReplicas": 1, "updatedReplicas": 1},
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
if path.startswith("/apis/apps/v1/statefulsets"):
|
|
|
|
|
return {
|
|
|
|
|
"items": [
|
|
|
|
|
{
|
|
|
|
|
"metadata": {"name": "db", "namespace": "apps"},
|
|
|
|
|
"spec": {"replicas": 1},
|
|
|
|
|
"status": {"readyReplicas": 1, "currentReplicas": 1, "updatedReplicas": 1},
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
if path.startswith("/apis/apps/v1/daemonsets"):
|
|
|
|
|
return {
|
|
|
|
|
"items": [
|
|
|
|
|
{
|
|
|
|
|
"metadata": {"name": "agent", "namespace": "apps"},
|
|
|
|
|
"status": {"desiredNumberScheduled": 3, "numberReady": 3, "updatedNumberScheduled": 3},
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
2026-01-26 03:31:42 -03:00
|
|
|
return {
|
|
|
|
|
"items": [
|
|
|
|
|
{
|
|
|
|
|
"metadata": {"name": "apps", "namespace": "flux-system"},
|
|
|
|
|
"spec": {"suspend": False},
|
|
|
|
|
"status": {"conditions": [{"type": "Ready", "status": "True"}]},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"metadata": {"name": "broken", "namespace": "flux-system"},
|
|
|
|
|
"spec": {"suspend": False},
|
|
|
|
|
"status": {"conditions": [{"type": "Ready", "status": "False", "reason": "Fail"}]},
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(cluster_state, "get_json", fake_get_json)
|
|
|
|
|
monkeypatch.setattr(cluster_state, "_vm_scalar", lambda _expr: 5.0)
|
|
|
|
|
monkeypatch.setattr(cluster_state, "_vm_vector", lambda _expr: [])
|
|
|
|
|
|
|
|
|
|
snapshot, summary = cluster_state.collect_cluster_state()
|
|
|
|
|
assert snapshot["nodes"]["total"] == 2
|
|
|
|
|
assert snapshot["nodes"]["ready"] == 1
|
|
|
|
|
assert snapshot["flux"]["not_ready"] == 1
|
2026-01-27 05:58:49 -03:00
|
|
|
assert snapshot["nodes_summary"]["total"] == 2
|
|
|
|
|
assert snapshot["nodes_summary"]["ready"] == 1
|
2026-01-29 01:52:58 -03:00
|
|
|
assert "pressure_nodes" in snapshot["nodes_summary"]
|
2026-01-27 05:41:46 -03:00
|
|
|
assert snapshot["nodes_detail"]
|
2026-01-29 03:02:39 -03:00
|
|
|
assert snapshot["nodes_detail"][1]["age_hours"] is not None
|
2026-01-29 03:08:57 -03:00
|
|
|
assert snapshot["nodes_detail"][1]["taints"]
|
2026-01-27 05:41:46 -03:00
|
|
|
assert snapshot["workloads"]
|
2026-01-28 20:34:58 -03:00
|
|
|
assert snapshot["namespace_pods"]
|
|
|
|
|
assert snapshot["namespace_pods"][0]["namespace"] == "media"
|
2026-01-28 22:33:26 -03:00
|
|
|
assert snapshot["namespace_nodes"]
|
|
|
|
|
assert snapshot["node_pods"]
|
2026-01-29 01:52:58 -03:00
|
|
|
assert "pod_issues" in snapshot
|
2026-01-29 02:39:08 -03:00
|
|
|
assert "workloads_health" in snapshot
|
|
|
|
|
assert snapshot["workloads_health"]["deployments"]["total"] == 1
|
|
|
|
|
assert snapshot["workloads_health"]["deployments"]["not_ready"] == 1
|
2026-01-29 02:31:30 -03:00
|
|
|
assert snapshot["events"]["warnings_total"] == 0
|
2026-01-28 20:41:17 -03:00
|
|
|
assert "node_usage_stats" in snapshot["metrics"]
|
|
|
|
|
assert snapshot["metrics"]["namespace_cpu_top"] == []
|
|
|
|
|
assert snapshot["metrics"]["namespace_mem_top"] == []
|
2026-01-29 03:04:42 -03:00
|
|
|
assert snapshot["metrics"]["namespace_cpu_requests_top"] == []
|
|
|
|
|
assert snapshot["metrics"]["namespace_mem_requests_top"] == []
|
2026-01-29 03:11:01 -03:00
|
|
|
assert snapshot["metrics"]["namespace_net_top"] == []
|
|
|
|
|
assert snapshot["metrics"]["namespace_io_top"] == []
|
2026-01-29 02:57:59 -03:00
|
|
|
assert snapshot["metrics"]["pod_cpu_top"] == []
|
|
|
|
|
assert snapshot["metrics"]["pod_mem_top"] == []
|
2026-01-29 03:00:00 -03:00
|
|
|
assert snapshot["metrics"]["job_failures_24h"] == []
|
2026-01-29 02:31:30 -03:00
|
|
|
assert snapshot["metrics"]["pvc_usage_top"] == []
|
2026-01-30 00:09:53 -03:00
|
|
|
assert snapshot["summary"]["counts"]["nodes_total"] == 5.0
|
|
|
|
|
assert snapshot["summary"]["counts"]["nodes_ready"] == 5.0
|
|
|
|
|
assert snapshot["summary"]["counts"]["pods_running"] == 5.0
|
|
|
|
|
assert snapshot["summary"]["top"]["namespace_pods"][0]["namespace"] == "media"
|
2026-01-30 02:16:32 -03:00
|
|
|
assert snapshot["summary"]["baseline_window"]
|
|
|
|
|
assert "workload_not_ready" in snapshot["summary"]["top"]
|
|
|
|
|
assert "pod_restarts" in snapshot["summary"]["top"]
|
|
|
|
|
assert "attention_ranked" in snapshot["summary"]
|
2026-01-30 00:09:53 -03:00
|
|
|
assert snapshot["summary"]["health_bullets"]
|
|
|
|
|
assert snapshot["summary"]["unknowns"] == []
|
|
|
|
|
assert snapshot["context"]["nodes"]
|
|
|
|
|
assert snapshot["context"]["namespaces"]
|
2026-01-30 02:16:32 -03:00
|
|
|
assert "baseline" in snapshot["context"]["nodes"][0]
|
2026-01-26 03:31:42 -03:00
|
|
|
assert summary.nodes_total == 2
|
|
|
|
|
assert summary.nodes_ready == 1
|
|
|
|
|
assert summary.pods_running == 5.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_run_cluster_state_records(monkeypatch) -> None:
|
|
|
|
|
dummy = DummyStorage()
|
|
|
|
|
snapshot = {"collected_at": datetime.now(timezone.utc).isoformat()}
|
|
|
|
|
summary = cluster_state.ClusterStateSummary(1, 1, 1.0, 0, 0)
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(cluster_state, "collect_cluster_state", lambda: (snapshot, summary))
|
|
|
|
|
|
|
|
|
|
result = cluster_state.run_cluster_state(dummy)
|
|
|
|
|
assert result == summary
|
|
|
|
|
assert dummy.snapshot == snapshot
|
|
|
|
|
assert dummy.keep == cluster_state.settings.cluster_state_keep
|