Files
2026-04-28 14:59:12 -04:00

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)