mirror of
https://github.com/Rainyy21/k3s-health-dashboard.git
synced 2026-10-11 01:40:19 -04:00
148 lines
4.4 KiB
Python
148 lines
4.4 KiB
Python
import os
|
|
from datetime import datetime, timezone
|
|
from kubernetes import client, config
|
|
|
|
_CONFIG_LOADED = False
|
|
|
|
def _ensure_config():
|
|
global _CONFIG_LOADED
|
|
if _CONFIG_LOADED:
|
|
return
|
|
try:
|
|
if os.getenv("IN_CLUSTER"):
|
|
config.load_incluster_config()
|
|
else:
|
|
# Load local kubeconfig
|
|
config_path = os.getenv("KUBECONFIG", os.path.expanduser("~/.kube/config"))
|
|
config.load_kube_config(config_file=config_path)
|
|
except Exception:
|
|
# If we can't load config (e.g. in tests where we mock), we don't want to crash here
|
|
# but let the client creation fail later or be mocked.
|
|
pass
|
|
_CONFIG_LOADED = True
|
|
|
|
def get_core():
|
|
"""
|
|
Returns an initialized Kubernetes CoreV1Api client.
|
|
Matches the name expected by test fixtures.
|
|
"""
|
|
_ensure_config()
|
|
return client.CoreV1Api()
|
|
|
|
def get_core_client() -> client.CoreV1Api:
|
|
"""Legacy alias for get_core() if needed."""
|
|
return get_core()
|
|
|
|
def get_custom_client() -> client.CustomObjectsApi:
|
|
_ensure_config()
|
|
return client.CustomObjectsApi()
|
|
|
|
def fetch_nodes():
|
|
v1 = get_core()
|
|
nodes = v1.list_node().items
|
|
result = []
|
|
for n in nodes:
|
|
conditions = {c.type: c.status for c in n.status.conditions} if n.status.conditions else {}
|
|
|
|
# Role logic
|
|
labels = n.metadata.labels or {}
|
|
roles = []
|
|
if "node-role.kubernetes.io/control-plane" in labels:
|
|
roles.append("control-plane")
|
|
if "node-role.kubernetes.io/master" in labels:
|
|
roles.append("master")
|
|
|
|
role_str = ", ".join(roles) if roles else "worker"
|
|
|
|
result.append({
|
|
"name": n.metadata.name,
|
|
"ready": conditions.get("Ready") == "True",
|
|
"roles": role_str,
|
|
"age": _format_age(n.metadata.creation_timestamp),
|
|
"cpu_cap": _parse_millicores(n.status.capacity.get("cpu", "0")),
|
|
"mem_cap_mi": _parse_mebibytes(n.status.capacity.get("memory", "0")),
|
|
"mem_alloc_mi": _parse_mebibytes(n.status.allocatable.get("memory", "0")),
|
|
"os": n.status.node_info.os_image,
|
|
"kubelet_version": n.status.node_info.kubelet_version,
|
|
"mem_pressure": conditions.get("MemoryPressure", "False"),
|
|
"disk_pressure": conditions.get("DiskPressure", "False"),
|
|
"pid_pressure": conditions.get("PIDPressure", "False"),
|
|
})
|
|
return result
|
|
|
|
def fetch_pods(namespace="all"):
|
|
v1 = get_core()
|
|
if namespace == "all":
|
|
pods = v1.list_pod_for_all_namespaces().items
|
|
else:
|
|
pods = v1.list_namespaced_pod(namespace=namespace).items
|
|
|
|
result = []
|
|
for p in pods:
|
|
result.append({
|
|
"namespace": p.metadata.namespace,
|
|
"name": p.metadata.name,
|
|
"ready": _is_pod_ready(p),
|
|
"phase": p.status.phase or "Unknown",
|
|
"restarts": _get_pod_restarts(p),
|
|
"node": p.spec.node_name or "—",
|
|
"age": _format_age(p.metadata.creation_timestamp),
|
|
})
|
|
return result
|
|
|
|
def _is_pod_ready(pod):
|
|
if not pod.status.conditions:
|
|
return False
|
|
for c in pod.status.conditions:
|
|
if c.type == "Ready":
|
|
return c.status == "True"
|
|
return False
|
|
|
|
def _get_pod_restarts(pod):
|
|
if not pod.status.container_statuses:
|
|
return 0
|
|
return sum(c.restart_count for c in pod.status.container_statuses)
|
|
|
|
def _format_age(ts):
|
|
if not ts:
|
|
return "—"
|
|
now = datetime.now(timezone.utc)
|
|
delta = now - ts
|
|
if delta.days > 0:
|
|
return f"{delta.days}d"
|
|
hours = delta.seconds // 3600
|
|
if hours > 0:
|
|
return f"{hours}h"
|
|
minutes = delta.seconds // 60
|
|
return f"{minutes}m"
|
|
|
|
def _parse_millicores(val):
|
|
if not val:
|
|
return 0.0
|
|
if isinstance(val, (int, float)):
|
|
return float(val)
|
|
if val.endswith("m"):
|
|
return float(val[:-1]) / 1000.0
|
|
return float(val)
|
|
|
|
def _parse_mebibytes(val):
|
|
if not val:
|
|
return 0.0
|
|
if isinstance(val, (int, float)):
|
|
return float(val)
|
|
|
|
# Simple parser for Ki, Mi, Gi, Ti
|
|
suffix_map = {
|
|
"Ki": 1/1024,
|
|
"Mi": 1,
|
|
"Gi": 1024,
|
|
"Ti": 1024*1024,
|
|
}
|
|
|
|
for suffix, multiplier in suffix_map.items():
|
|
if val.endswith(suffix):
|
|
return float(val[:-len(suffix)]) * multiplier
|
|
|
|
# Bytes to MiB if no suffix
|
|
return float(val) / (1024 * 1024)
|