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
Build and Publish Docker Image / Build and Push Docker Image (push) Successful in 1m42s
Details
This commit is contained in:
parent
e73e5257c9
commit
dedac92afa
|
|
@ -190,3 +190,4 @@ pyrightconfig.json
|
|||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/python,linux
|
||||
myvalues.yaml
|
||||
|
||||
|
|
|
|||
169
app.py
169
app.py
|
|
@ -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():
|
||||
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
|
||||
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": []
|
||||
}
|
||||
|
||||
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,42 +201,41 @@ def cluster_data():
|
|||
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
|
||||
for pod in node_pods:
|
||||
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
|
||||
|
||||
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)
|
||||
# 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_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
|
||||
})
|
||||
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)
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 3.0 MiB |
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
[metadata]
|
||||
version = 0.7
|
||||
version = 0.8
|
||||
|
|
|
|||
Loading…
Reference in New Issue