0.8 - fix health check, added pod counts on node
Build and Publish Docker Image / Build and Push Docker Image (push) Successful in 1m42s Details

This commit is contained in:
Isaac Johnson 2026-07-29 05:40:29 -05:00
parent e73e5257c9
commit dedac92afa
7 changed files with 124 additions and 64 deletions

1
.gitignore vendored
View File

@ -190,3 +190,4 @@ pyrightconfig.json
# End of https://www.toptal.com/developers/gitignore/api/python,linux
myvalues.yaml

93
app.py
View File

@ -1,5 +1,6 @@
import os
import configparser
from concurrent.futures import ThreadPoolExecutor
from flask import Flask, jsonify, render_template
from kubernetes import client, config
from kubernetes.client.rest import ApiException
@ -17,7 +18,13 @@ def get_version():
app = Flask(__name__)
_v1_client = None
_custom_client = None
def get_k8s_client():
global _v1_client, _custom_client
if _v1_client is not None and _custom_client is not None:
return _v1_client, _custom_client
try:
config.load_incluster_config()
except config.ConfigException:
@ -36,7 +43,9 @@ def get_k8s_client():
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()
_v1_client = client.CoreV1Api()
_custom_client = client.CustomObjectsApi()
return _v1_client, _custom_client
def parse_cpu(cpu_str):
if not cpu_str: return 0
@ -64,32 +73,62 @@ def parse_memory(mem_str):
except:
return 0
K8S_TIMEOUT = 5.0
METRICS_TIMEOUT = 3.0
def fetch_nodes(v1):
return v1.list_node(_request_timeout=K8S_TIMEOUT).items
def fetch_pods(v1):
return v1.list_pod_for_all_namespaces(_request_timeout=K8S_TIMEOUT).items
def fetch_node_metrics(custom):
node_metrics = {}
try:
nm = custom.list_cluster_custom_object("metrics.k8s.io", "v1beta1", "nodes", _request_timeout=METRICS_TIMEOUT)
for item in nm.get('items', []):
node_metrics[item['metadata']['name']] = item['usage']
except Exception as e:
print(f"Metrics server node fetch error or unavailable: {e}")
return node_metrics
def fetch_pod_metrics(custom):
pod_metrics = {}
try:
pm = custom.list_cluster_custom_object("metrics.k8s.io", "v1beta1", "pods", _request_timeout=METRICS_TIMEOUT)
for item in pm.get('items', []):
key = f"{item['metadata']['namespace']}/{item['metadata']['name']}"
pod_metrics[key] = item['containers']
except Exception as e:
print(f"Metrics server pod fetch error or unavailable: {e}")
return pod_metrics
@app.route('/api/cluster-data')
def cluster_data():
try:
v1, custom = get_k8s_client()
with ThreadPoolExecutor(max_workers=4) as executor:
fut_nodes = executor.submit(fetch_nodes, v1)
fut_pods = executor.submit(fetch_pods, v1)
fut_node_metrics = executor.submit(fetch_node_metrics, custom)
fut_pod_metrics = executor.submit(fetch_pod_metrics, custom)
nodes = fut_nodes.result()
pods = fut_pods.result()
node_metrics = fut_node_metrics.result()
pod_metrics = fut_pod_metrics.result()
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
pods_by_node = {}
for pod in pods:
nname = pod.spec.node_name
if nname:
pods_by_node.setdefault(nname, []).append(pod)
for node in nodes:
node_name = node.metadata.name
@ -131,6 +170,13 @@ def cluster_data():
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_pods = pods_by_node.get(node_name, [])
non_terminated_pods = [
p for p in node_pods
if p.status and getattr(p.status, 'phase', None) not in ['Succeeded', 'Failed']
]
non_terminated_pod_count = len(non_terminated_pods)
node_data = {
"name": node_name,
"type": "node",
@ -141,6 +187,10 @@ def cluster_data():
"internalIp": internal_ip,
"os_image": os_image,
"osImage": os_image,
"non_terminated_pods": non_terminated_pod_count,
"nonTerminatedPods": non_terminated_pod_count,
"pod_count": non_terminated_pod_count,
"podCount": non_terminated_pod_count,
"cpu_allocatable": cpu_allocatable,
"mem_allocatable": mem_allocatable,
"cpu_usage": node_usage_cpu,
@ -151,8 +201,7 @@ def cluster_data():
node_cpu_total = 0
node_mem_total = 0
for pod in pods:
if pod.spec.node_name == node_name:
for pod in node_pods:
pod_cpu_req = 0
pod_mem_req = 0
@ -217,6 +266,10 @@ def cluster_data():
def version_route():
return jsonify({"version": get_version()})
@app.route('/healthz')
def healthz():
return jsonify({"status": "ok"}), 200
@app.route('/')
def index():
return render_template('index.html', version=get_version())

View File

@ -2,5 +2,5 @@ apiVersion: v2
name: k8s-pulse
description: A Helm chart for k8s-pulse, a Kubernetes cluster resource visualizer.
type: application
version: 0.1.0
appVersion: "1.0.4"
version: 0.1.1
appVersion: "1.0.8"

View File

@ -42,13 +42,13 @@ spec:
protocol: TCP
livenessProbe:
httpGet:
path: /
path: /healthz
port: http
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 10

BIN
static/2026-07-herdrbg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

View File

@ -405,7 +405,8 @@ function build3DCluster(data) {
isControlPlane: isControlPlane,
roles: n.data.roles || (isControlPlane ? ['control-plane'] : ['worker']),
internalIp: n.data.internalIp || n.data.internal_ip || 'N/A',
osImage: n.data.osImage || n.data.os_image || 'N/A'
osImage: n.data.osImage || n.data.os_image || 'N/A',
nonTerminatedPods: n.data.nonTerminatedPods ?? n.data.non_terminated_pods ?? n.data.podCount ?? n.data.pod_count ?? (n.data.children ? n.data.children.filter(c => c.type === 'pod' && c.status !== 'Succeeded' && c.status !== 'Failed').length : 0)
};
platformMesh.userData = nodeUserData;
@ -601,6 +602,7 @@ function onPointerMove(event) {
if (data.type === 'node') {
const isCP = data.isControlPlane || (data.roles && (data.roles.includes('control-plane') || data.roles.includes('master')));
const badgeLabel = data.roles && data.roles.length ? data.roles.join(', ') : (isCP ? 'control-plane' : 'Node');
const podCount = data.nonTerminatedPods ?? data.non_terminated_pods ?? data.podCount ?? data.pod_count ?? 0;
tooltip.innerHTML = `
<div class="tooltip-title">
<span>${data.name}</span>
@ -614,6 +616,10 @@ function onPointerMove(event) {
<span class="tooltip-label">OS Image:</span>
<span class="tooltip-value">${data.osImage || 'N/A'}</span>
</div>
<div class="tooltip-row">
<span class="tooltip-label">Non-terminated Pods:</span>
<span class="tooltip-value">${podCount}</span>
</div>
`;
} else if (data.type === 'unused') {
const valCpu = formatCpu(data.cpu_raw);

View File

@ -1,2 +1,2 @@
[metadata]
version = 0.7
version = 0.8