k8s-pulse/app.py

226 lines
8.6 KiB
Python

import os
import configparser
from flask import Flask, jsonify, render_template
from kubernetes import client, config
from kubernetes.client.rest import ApiException
def get_version():
try:
ini_path = os.path.join(os.path.dirname(__file__), 'version.ini')
if os.path.exists(ini_path):
parser = configparser.ConfigParser()
parser.read(ini_path)
return parser.get('metadata', 'version', fallback='0.5')
except Exception as e:
print(f"Error reading version.ini: {e}")
return '0.5'
app = Flask(__name__)
def get_k8s_client():
try:
config.load_incluster_config()
except config.ConfigException:
try:
config.load_kube_config()
except Exception as e:
print(f"Error loading kubeconfig: {e}")
loaded = False
for fallback_path in ["/home/nonroot/.kube/config", "/root/.kube/config"]:
try:
if os.path.exists(fallback_path):
config.load_kube_config(config_file=fallback_path)
loaded = True
break
except Exception:
pass
if not loaded:
raise RuntimeError(f"Failed to load kubeconfig. Make sure the file exists and is accessible. Error: {e}")
return client.CoreV1Api(), client.CustomObjectsApi()
def parse_cpu(cpu_str):
if not cpu_str: return 0
cpu_str = str(cpu_str)
if cpu_str.endswith('m'):
return int(cpu_str[:-1])
if cpu_str.endswith('n'):
return int(cpu_str[:-1]) // 1000000
try:
return int(float(cpu_str) * 1000)
except:
return 0
def parse_memory(mem_str):
if not mem_str: return 0
mem_str = str(mem_str)
if mem_str.endswith('Ki'):
return int(mem_str[:-2]) * 1024
if mem_str.endswith('Mi'):
return int(mem_str[:-2]) * 1024**2
if mem_str.endswith('Gi'):
return int(mem_str[:-2]) * 1024**3
try:
return int(mem_str)
except:
return 0
@app.route('/api/cluster-data')
def cluster_data():
v1, custom = get_k8s_client()
cluster = {
"name": "Cluster",
"children": []
}
try:
nodes = v1.list_node().items
pods = v1.list_pod_for_all_namespaces().items
node_metrics = {}
pod_metrics = {}
try:
nm = custom.list_cluster_custom_object("metrics.k8s.io", "v1beta1", "nodes")
for item in nm.get('items', []):
node_metrics[item['metadata']['name']] = item['usage']
pm = custom.list_cluster_custom_object("metrics.k8s.io", "v1beta1", "pods")
for item in pm.get('items', []):
key = f"{item['metadata']['namespace']}/{item['metadata']['name']}"
pod_metrics[key] = item['containers']
except ApiException:
pass # Metrics server not available
for node in nodes:
node_name = node.metadata.name
internal_ip = "N/A"
if node.status and getattr(node.status, 'addresses', None):
for addr in node.status.addresses:
addr_type = getattr(addr, 'type', None) or (addr.get('type') if isinstance(addr, dict) else None)
addr_val = getattr(addr, 'address', None) or (addr.get('address') if isinstance(addr, dict) else None)
if addr_type == 'InternalIP':
internal_ip = addr_val
break
os_image = "N/A"
if node.status and getattr(node.status, 'node_info', None):
node_info = node.status.node_info
if isinstance(node_info, dict):
os_image = node_info.get('osImage') or node_info.get('os_image') or "N/A"
else:
os_image = getattr(node_info, 'os_image', None) or getattr(node_info, 'os_image', "N/A")
labels = node.metadata.labels or {}
roles = []
for k in labels:
if k.startswith("node-role.kubernetes.io/"):
role_name = k.split("/", 1)[1]
if role_name:
roles.append(role_name)
is_control_plane = any(r in ["control-plane", "master"] for r in roles) or ("control-plane" in node_name) or ("master" in node_name)
if is_control_plane and not roles:
roles = ["control-plane" if "control-plane" in node_name else "master"]
elif not roles:
roles = ["worker"]
cpu_allocatable = parse_cpu(node.status.allocatable.get('cpu', '0'))
mem_allocatable = parse_memory(node.status.allocatable.get('memory', '0'))
node_usage_cpu = parse_cpu(node_metrics.get(node_name, {}).get('cpu', '0'))
node_usage_mem = parse_memory(node_metrics.get(node_name, {}).get('memory', '0'))
node_data = {
"name": node_name,
"type": "node",
"roles": roles,
"is_control_plane": is_control_plane,
"isControlPlane": is_control_plane,
"internal_ip": internal_ip,
"internalIp": internal_ip,
"os_image": os_image,
"osImage": os_image,
"cpu_allocatable": cpu_allocatable,
"mem_allocatable": mem_allocatable,
"cpu_usage": node_usage_cpu,
"mem_usage": node_usage_mem,
"children": []
}
node_cpu_total = 0
node_mem_total = 0
for pod in pods:
if pod.spec.node_name == node_name:
pod_cpu_req = 0
pod_mem_req = 0
if pod.spec.containers:
for c in pod.spec.containers:
if c.resources and c.resources.requests:
pod_cpu_req += parse_cpu(c.resources.requests.get('cpu', '0'))
pod_mem_req += parse_memory(c.resources.requests.get('memory', '0'))
key = f"{pod.metadata.namespace}/{pod.metadata.name}"
if key in pod_metrics:
pod_cpu = sum([parse_cpu(c['usage'].get('cpu', '0')) for c in pod_metrics[key]])
pod_mem = sum([parse_memory(c['usage'].get('memory', '0')) for c in pod_metrics[key]])
else:
pod_cpu = pod_cpu_req
pod_mem = pod_mem_req
# D3 Treemap doesn't like 0 values, so minimum size 1
pod_cpu_val = max(pod_cpu, 1)
pod_mem_val = max(pod_mem, 1)
node_cpu_total += pod_cpu
node_mem_total += pod_mem
node_data["children"].append({
"name": pod.metadata.name,
"namespace": pod.metadata.namespace,
"type": "pod",
"cpu": pod_cpu_val,
"memory": pod_mem_val,
"cpu_raw": pod_cpu,
"memory_raw": pod_mem,
"status": pod.status.phase
})
# Optionally add an "Unused" block so the node size matches allocatable
unused_cpu = max(0, cpu_allocatable - node_cpu_total)
unused_mem = max(0, mem_allocatable - node_mem_total)
if unused_cpu > 0 or unused_mem > 0:
node_data["children"].append({
"name": "Available Capacity",
"namespace": "",
"type": "unused",
"cpu": max(unused_cpu, 1),
"memory": max(unused_mem, 1),
"cpu_raw": unused_cpu,
"memory_raw": unused_mem,
"status": "Available"
})
cluster["children"].append(node_data)
return jsonify(cluster)
except Exception as e:
import traceback
traceback.print_exc()
return jsonify({"error": str(e)}), 500
@app.route('/api/version')
def version_route():
return jsonify({"version": get_version()})
@app.route('/')
def index():
return render_template('index.html', version=get_version())
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)