mirror of
https://github.com/YuzuZensai/Minikura.git
synced 2026-03-30 14:25:37 +00:00
366 lines
10 KiB
TypeScript
366 lines
10 KiB
TypeScript
import type {
|
|
ConnectionInfo,
|
|
K8sNodeSummary,
|
|
NormalServer,
|
|
PodInfo,
|
|
ReverseProxyServer,
|
|
} from "@minikura/api";
|
|
import { parseK8sMetrics } from "./k8s-metrics";
|
|
import type {
|
|
HealthStatus,
|
|
K8sNodeMetadata,
|
|
ProxyMetadata,
|
|
ResourceMetrics,
|
|
ServerMetadata,
|
|
TopologyEdge,
|
|
TopologyGraph,
|
|
TopologyNode,
|
|
} from "./topology-types";
|
|
|
|
function parsePodReady(ready: string): { ready: number; total: number } {
|
|
const [readyStr, totalStr] = ready.split("/");
|
|
return {
|
|
ready: parseInt(readyStr, 10) || 0,
|
|
total: parseInt(totalStr, 10) || 0,
|
|
};
|
|
}
|
|
|
|
function calculateHealthStatus(readyPods: number, totalPods: number): HealthStatus {
|
|
if (totalPods === 0) return "unknown";
|
|
if (readyPods === totalPods) return "healthy";
|
|
if (readyPods > 0) return "degraded";
|
|
return "unhealthy";
|
|
}
|
|
|
|
function getPodMetrics(podName: string, podMetrics: any): ResourceMetrics | undefined {
|
|
if (!podMetrics?.items) return undefined;
|
|
|
|
const metric = podMetrics.items.find((m: any) => m.metadata?.name === podName);
|
|
if (!metric?.containers?.[0]?.usage) return undefined;
|
|
|
|
const usage = metric.containers[0].usage;
|
|
return parseK8sMetrics(usage.cpu, usage.memory);
|
|
}
|
|
|
|
function getNodeMetrics(nodeName: string, nodeMetrics: any): ResourceMetrics | undefined {
|
|
if (!nodeMetrics?.items) return undefined;
|
|
|
|
const metric = nodeMetrics.items.find((m: any) => m.metadata?.name === nodeName);
|
|
if (!metric?.usage) return undefined;
|
|
|
|
return parseK8sMetrics(metric.usage.cpu, metric.usage.memory);
|
|
}
|
|
|
|
interface BuildEnhancedGraphInput {
|
|
servers: NormalServer[];
|
|
proxies: ReverseProxyServer[];
|
|
serverPods: Map<string, PodInfo[]>;
|
|
proxyPods: Map<string, PodInfo[]>;
|
|
k8sNodes: K8sNodeSummary[];
|
|
serverConnections?: Map<string, ConnectionInfo | null>;
|
|
proxyConnections?: Map<string, ConnectionInfo | null>;
|
|
podMetrics?: any;
|
|
nodeMetrics?: any;
|
|
}
|
|
|
|
function parseProxyServerConnections(
|
|
_proxy: ReverseProxyServer,
|
|
allServers: NormalServer[]
|
|
): string[] {
|
|
return allServers.map((s) => s.id);
|
|
}
|
|
|
|
export function buildTopologyGraph(input: BuildEnhancedGraphInput): TopologyGraph {
|
|
const {
|
|
servers,
|
|
proxies,
|
|
serverPods,
|
|
proxyPods,
|
|
k8sNodes,
|
|
serverConnections,
|
|
proxyConnections,
|
|
podMetrics,
|
|
nodeMetrics,
|
|
} = input;
|
|
|
|
const nodes: TopologyNode[] = [];
|
|
const edges: TopologyEdge[] = [];
|
|
|
|
let healthySystems = 0;
|
|
let degradedSystems = 0;
|
|
let unhealthySystems = 0;
|
|
|
|
const serverToProxies = new Map<string, string[]>();
|
|
const proxyToServers = new Map<string, string[]>();
|
|
const serverToK8sNodes = new Map<string, string[]>();
|
|
const proxyToK8sNodes = new Map<string, string[]>();
|
|
const k8sNodeToPods = new Map<string, { servers: string[]; proxies: string[] }>();
|
|
|
|
for (const node of k8sNodes) {
|
|
if (node.name) {
|
|
k8sNodeToPods.set(node.name, { servers: [], proxies: [] });
|
|
}
|
|
}
|
|
|
|
for (const proxy of proxies) {
|
|
const connectedServerIds = parseProxyServerConnections(proxy, servers);
|
|
proxyToServers.set(proxy.id, connectedServerIds);
|
|
|
|
for (const serverId of connectedServerIds) {
|
|
const existing = serverToProxies.get(serverId) || [];
|
|
existing.push(proxy.id);
|
|
serverToProxies.set(serverId, existing);
|
|
}
|
|
}
|
|
|
|
for (const [serverId, pods] of serverPods.entries()) {
|
|
const nodeNames = new Set<string>();
|
|
for (const pod of pods) {
|
|
if (pod.nodeName) {
|
|
nodeNames.add(pod.nodeName);
|
|
const nodeData = k8sNodeToPods.get(pod.nodeName);
|
|
if (nodeData) {
|
|
nodeData.servers.push(pod.name);
|
|
}
|
|
}
|
|
}
|
|
serverToK8sNodes.set(serverId, Array.from(nodeNames));
|
|
}
|
|
|
|
for (const [proxyId, pods] of proxyPods.entries()) {
|
|
const nodeNames = new Set<string>();
|
|
for (const pod of pods) {
|
|
if (pod.nodeName) {
|
|
nodeNames.add(pod.nodeName);
|
|
const nodeData = k8sNodeToPods.get(pod.nodeName);
|
|
if (nodeData) {
|
|
nodeData.proxies.push(pod.name);
|
|
}
|
|
}
|
|
}
|
|
proxyToK8sNodes.set(proxyId, Array.from(nodeNames));
|
|
}
|
|
|
|
for (const server of servers) {
|
|
const pods = serverPods.get(server.id) || [];
|
|
const readyPods = pods.filter((p) => {
|
|
const { ready, total } = parsePodReady(p.ready);
|
|
return ready === total && p.status.toLowerCase() === "running";
|
|
}).length;
|
|
const health = calculateHealthStatus(readyPods, pods.length);
|
|
|
|
if (health === "healthy") healthySystems++;
|
|
else if (health === "degraded") degradedSystems++;
|
|
else if (health === "unhealthy") unhealthySystems++;
|
|
|
|
const podMetric = pods[0] ? getPodMetrics(pods[0].name, podMetrics) : undefined;
|
|
|
|
const metadata: ServerMetadata = {
|
|
server,
|
|
podCount: pods.length,
|
|
readyPods,
|
|
pods,
|
|
health,
|
|
connectedProxies: serverToProxies.get(server.id) || [],
|
|
k8sNodes: serverToK8sNodes.get(server.id) || [],
|
|
connectionInfo: serverConnections?.get(server.id) || null,
|
|
metrics: podMetric,
|
|
};
|
|
|
|
nodes.push({
|
|
id: `server-${server.id}`,
|
|
type: "server",
|
|
position: { x: 0, y: 0 },
|
|
data: {
|
|
id: server.id,
|
|
type: "server",
|
|
label: server.id,
|
|
status: health,
|
|
metadata,
|
|
},
|
|
});
|
|
}
|
|
|
|
for (const proxy of proxies) {
|
|
const pods = proxyPods.get(proxy.id) || [];
|
|
const readyPods = pods.filter((p) => {
|
|
const { ready, total } = parsePodReady(p.ready);
|
|
return ready === total && p.status.toLowerCase() === "running";
|
|
}).length;
|
|
const health = calculateHealthStatus(readyPods, pods.length);
|
|
|
|
if (health === "healthy") healthySystems++;
|
|
else if (health === "degraded") degradedSystems++;
|
|
else if (health === "unhealthy") unhealthySystems++;
|
|
|
|
const connectedServers = proxyToServers.get(proxy.id) || [];
|
|
const proxyPodMetric = pods[0] ? getPodMetrics(pods[0].name, podMetrics) : undefined;
|
|
|
|
const metadata: ProxyMetadata = {
|
|
proxy,
|
|
podCount: pods.length,
|
|
readyPods,
|
|
pods,
|
|
health,
|
|
connectedServers,
|
|
k8sNodes: proxyToK8sNodes.get(proxy.id) || [],
|
|
connectionInfo: proxyConnections?.get(proxy.id) || null,
|
|
metrics: proxyPodMetric,
|
|
};
|
|
|
|
nodes.push({
|
|
id: `proxy-${proxy.id}`,
|
|
type: "proxy",
|
|
position: { x: 0, y: 0 },
|
|
data: {
|
|
id: proxy.id,
|
|
type: "proxy",
|
|
label: proxy.id,
|
|
status: health,
|
|
metadata,
|
|
},
|
|
});
|
|
|
|
for (const serverId of connectedServers) {
|
|
edges.push({
|
|
id: `edge-proxy-${proxy.id}-server-${serverId}`,
|
|
source: `proxy-${proxy.id}`,
|
|
target: `server-${serverId}`,
|
|
type: "smoothstep",
|
|
animated: false,
|
|
data: {
|
|
id: `edge-proxy-${proxy.id}-server-${serverId}`,
|
|
source: `proxy-${proxy.id}`,
|
|
target: `server-${serverId}`,
|
|
type: "proxy-to-server",
|
|
label: "routes to",
|
|
animated: false,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
for (const k8sNode of k8sNodes) {
|
|
if (!k8sNode.name) continue;
|
|
|
|
const nodePods = k8sNodeToPods.get(k8sNode.name);
|
|
const podCount = (nodePods?.servers.length || 0) + (nodePods?.proxies.length || 0);
|
|
|
|
let nodeHealth: HealthStatus = "healthy";
|
|
if (k8sNode.status.toLowerCase() !== "ready") {
|
|
nodeHealth = "unhealthy";
|
|
}
|
|
|
|
const k8sNodeMetric = getNodeMetrics(k8sNode.name, nodeMetrics);
|
|
|
|
const metadata: K8sNodeMetadata = {
|
|
node: k8sNode,
|
|
podCount,
|
|
serverPods: nodePods?.servers || [],
|
|
proxyPods: nodePods?.proxies || [],
|
|
health: nodeHealth,
|
|
metrics: k8sNodeMetric,
|
|
};
|
|
|
|
nodes.push({
|
|
id: `k8s-node-${k8sNode.name}`,
|
|
type: "k8s-node",
|
|
position: { x: 0, y: 0 },
|
|
data: {
|
|
id: k8sNode.name,
|
|
type: "k8s-node",
|
|
label: k8sNode.name,
|
|
status: nodeHealth,
|
|
metadata,
|
|
},
|
|
});
|
|
|
|
for (const [serverId, k8sNodeNames] of serverToK8sNodes.entries()) {
|
|
if (k8sNodeNames.includes(k8sNode.name)) {
|
|
edges.push({
|
|
id: `edge-k8s-${k8sNode.name}-server-${serverId}`,
|
|
source: `k8s-node-${k8sNode.name}`,
|
|
target: `server-${serverId}`,
|
|
type: "smoothstep",
|
|
animated: false,
|
|
data: {
|
|
id: `edge-k8s-${k8sNode.name}-server-${serverId}`,
|
|
source: `k8s-node-${k8sNode.name}`,
|
|
target: `server-${serverId}`,
|
|
type: "pod-to-node",
|
|
label: "hosts",
|
|
animated: false,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
for (const [proxyId, k8sNodeNames] of proxyToK8sNodes.entries()) {
|
|
if (k8sNodeNames.includes(k8sNode.name)) {
|
|
edges.push({
|
|
id: `edge-k8s-${k8sNode.name}-proxy-${proxyId}`,
|
|
source: `k8s-node-${k8sNode.name}`,
|
|
target: `proxy-${proxyId}`,
|
|
type: "smoothstep",
|
|
animated: false,
|
|
data: {
|
|
id: `edge-k8s-${k8sNode.name}-proxy-${proxyId}`,
|
|
source: `k8s-node-${k8sNode.name}`,
|
|
target: `proxy-${proxyId}`,
|
|
type: "pod-to-node",
|
|
label: "hosts",
|
|
animated: false,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
nodes,
|
|
edges,
|
|
metadata: {
|
|
totalServers: servers.length,
|
|
totalProxies: proxies.length,
|
|
totalK8sNodes: k8sNodes.length,
|
|
totalConnections: edges.length,
|
|
healthySystems,
|
|
degradedSystems,
|
|
unhealthySystems,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function filterNodes(
|
|
nodes: TopologyNode[],
|
|
filters: {
|
|
showServers: boolean;
|
|
showProxies: boolean;
|
|
showK8sNodes: boolean;
|
|
searchQuery: string;
|
|
filterByStatus?: HealthStatus;
|
|
}
|
|
): TopologyNode[] {
|
|
return nodes.filter((node) => {
|
|
const { type, label, status } = node.data;
|
|
|
|
if (type === "server" && !filters.showServers) return false;
|
|
if (type === "proxy" && !filters.showProxies) return false;
|
|
if (type === "k8s-node" && !filters.showK8sNodes) return false;
|
|
|
|
if (filters.searchQuery && !label.toLowerCase().includes(filters.searchQuery.toLowerCase())) {
|
|
return false;
|
|
}
|
|
|
|
if (filters.filterByStatus && status !== filters.filterByStatus) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
export function filterEdges(edges: TopologyEdge[], visibleNodeIds: Set<string>): TopologyEdge[] {
|
|
return edges.filter((edge) => visibleNodeIds.has(edge.source) && visibleNodeIds.has(edge.target));
|
|
}
|