refactor: 全局重命名 k8s → kubernetes (前后端+路由+表名+文件)
后端: - pkg/k8sclient → pkg/kubernetesclient - models/k8s.go → models/kubernetes.go - handlers/k8s.go → handlers/kubernetes.go - K8sCluster/K8sNode/.. → KubernetesCluster/KubernetesNode/.. - K8sHandler → KubernetesHandler - 路由: /k8s-clusters → /kubernetes-clusters - 路由: /k8s/:code/namespace/:ns/... → /kubernetes/:code/namespace/:ns/... (Kuboard风格) - 表前缀: container_k8s_ → container_kubernetes_ 前端: - components/k8s/ → components/kubernetes/ - K8sClusters → KubernetesClusters - K8sWorkloads → KubernetesWorkloads - K8sNodes → KubernetesNodes - k8sApi → kubernetesApi - 路由前缀: /k8s/ → /kubernetes/ 测试验证: ✅ /kubernetes/k8s-cluster-liren/namespace/kube-system/deployments → 3 items ✅ /kubernetes/k8s-cluster-liren/namespace/kube-system/pods → 7 items ✅ /kubernetes/k8s-cluster-liren/namespace/kube-system/services → 4 items ✅ /kubernetes/k8s-cluster-liren/namespace/kube-system/configmaps → 6 items ✅ /kubernetes/k8s-cluster-liren/namespace/kube-system/secrets → 1 item ✅ /kubernetes/k8s-cluster-liren/namespaces → 4 items
This commit is contained in:
1 parent
f7b667b878
commit
cac1414917
27 files changed
+779
-284
No files matched your search
@@ -0,0 +1,181 @@
|
||||
import {
|
||||
Network, Server, Globe, Cloud, Boxes, Database, Zap, Box,
|
||||
} from 'lucide-react';
|
||||
import type { TopologyNode, TopologyEdge } from '@/types';
|
||||
|
||||
// K8s 资源类型 → 拓扑节点类型标签 // Logan.Li
|
||||
export const NODE_TYPE_LABELS: Record<string, string> = {
|
||||
cluster: '集群',
|
||||
loadbalancer: 'Ingress',
|
||||
service: 'Service',
|
||||
app: 'Deployment',
|
||||
server: 'Node',
|
||||
database: '数据库',
|
||||
external: '外部服务',
|
||||
custom: '自定义',
|
||||
};
|
||||
|
||||
// 自定义节点类型选项(编辑模式用)
|
||||
export const CUSTOM_NODE_TYPES = [
|
||||
{ value: 'database', label: '数据库', desc: 'MySQL / Redis / PostgreSQL 等' },
|
||||
{ value: 'external', label: '外部服务', desc: '外部 API / CDN / DNS 等' },
|
||||
{ value: 'custom', label: '自定义', desc: '其他自定义节点' },
|
||||
];
|
||||
|
||||
// 判断是否为自定义节点(非 K8s 原生资源)
|
||||
export const isCustomNode = (id: string) => id.startsWith('custom-');
|
||||
|
||||
// 获取节点图标
|
||||
export function getNodeIcon(type: string, className = 'h-5 w-5') {
|
||||
switch (type) {
|
||||
case 'server': return <Server className={className} />;
|
||||
case 'loadbalancer': return <Globe className={className} />;
|
||||
case 'cluster': return <Cloud className={className} />;
|
||||
case 'app': return <Boxes className={className} />;
|
||||
case 'service': return <Network className={className} />;
|
||||
case 'database': return <Database className={className} />;
|
||||
case 'external': return <Zap className={className} />;
|
||||
case 'custom': return <Box className={className} />;
|
||||
default: return <Server className={className} />;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取节点颜色样式
|
||||
export function getNodeColor(type: string) {
|
||||
switch (type) {
|
||||
case 'server': return 'bg-blue-500/20 text-blue-400 border-blue-500/30';
|
||||
case 'loadbalancer': return 'bg-purple-500/20 text-purple-400 border-purple-500/30';
|
||||
case 'cluster': return 'bg-cyan-500/20 text-cyan-400 border-cyan-500/30';
|
||||
case 'app': return 'bg-rose-500/20 text-rose-400 border-rose-500/30';
|
||||
case 'service': return 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30';
|
||||
case 'database': return 'bg-amber-500/20 text-amber-400 border-amber-500/30';
|
||||
case 'external': return 'bg-orange-500/20 text-orange-400 border-orange-500/30';
|
||||
case 'custom': return 'bg-slate-500/20 text-slate-400 border-slate-500/30';
|
||||
default: return 'bg-slate-500/20 text-slate-400 border-slate-500/30';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* buildTopology — 从真实 K8s 资源构建分层拓扑图
|
||||
* 层级: Cluster → Ingress → Service → Deployment → Node
|
||||
*/
|
||||
export function buildTopology(
|
||||
cluster: any,
|
||||
ingresses: any[],
|
||||
services: any[],
|
||||
deployments: any[],
|
||||
nodes: any[],
|
||||
): { nodes: TopologyNode[]; edges: TopologyEdge[] } {
|
||||
const tNodes: TopologyNode[] = [];
|
||||
const tEdges: TopologyEdge[] = [];
|
||||
const MAX_PER_LAYER = 12;
|
||||
|
||||
const maxLayerCount = Math.max(1, ingresses.length, services.length, deployments.length, nodes.length);
|
||||
const canvasW = Math.max(900, Math.min(maxLayerCount * 130, 2400));
|
||||
const layerY = [60, 180, 320, 480, 640];
|
||||
|
||||
const distributeX = (count: number): number[] => {
|
||||
const cap = Math.min(count, MAX_PER_LAYER);
|
||||
if (cap === 0) return [];
|
||||
if (cap === 1) return [canvasW / 2];
|
||||
const step = canvasW / (cap + 1);
|
||||
return Array.from({ length: cap }, (_, i) => step * (i + 1));
|
||||
};
|
||||
|
||||
// Layer 0: Cluster
|
||||
const clusterId = `cluster-${cluster.id}`;
|
||||
tNodes.push({
|
||||
id: clusterId, name: cluster.name || '集群', type: 'cluster',
|
||||
x: canvasW / 2, y: layerY[0],
|
||||
data: { kind: 'cluster', version: cluster.version, status: cluster.status, api_server: cluster.api_server, node_count: cluster.node_count, pod_count: cluster.pod_count },
|
||||
});
|
||||
|
||||
// Layer 1: Ingress
|
||||
const ingCap = ingresses.slice(0, MAX_PER_LAYER);
|
||||
const ingXs = distributeX(ingCap.length);
|
||||
ingCap.forEach((ing, i) => {
|
||||
const id = `ingress-${ing.id}`;
|
||||
tNodes.push({ id, name: ing.name, type: 'loadbalancer', x: ingXs[i], y: layerY[1], data: { kind: 'ingress', namespace: ing.namespace, backend: ing.backend, tls: ing.tls, hosts: ing.hosts } });
|
||||
tEdges.push({ id: `e-${clusterId}-${id}`, source: clusterId, target: id, label: 'route' });
|
||||
});
|
||||
|
||||
// Layer 2: Service
|
||||
const svcCap = services.slice(0, MAX_PER_LAYER);
|
||||
const svcXs = distributeX(svcCap.length);
|
||||
svcCap.forEach((svc, i) => {
|
||||
const id = `service-${svc.id}`;
|
||||
tNodes.push({ id, name: svc.name, type: 'service', x: svcXs[i], y: layerY[2], data: { kind: 'service', namespace: svc.namespace, type: svc.type, cluster_ip: svc.cluster_ip, external_ip: svc.external_ip, selector: svc.selector } });
|
||||
});
|
||||
|
||||
// Ingress → Service edges
|
||||
if (ingCap.length > 0 && svcCap.length > 0) {
|
||||
ingCap.forEach((ing) => {
|
||||
const ingNodeId = `ingress-${ing.id}`;
|
||||
const backendSvc = (ing.backend || '').split(':')[0].trim();
|
||||
let matched = svcCap.filter((s) => s.name === backendSvc);
|
||||
if (matched.length === 0) matched = svcCap.filter((s) => s.namespace === ing.namespace);
|
||||
if (matched.length === 0) matched = [svcCap[0]];
|
||||
matched.forEach((svc) => tEdges.push({ id: `e-${ingNodeId}-service-${svc.id}`, source: ingNodeId, target: `service-${svc.id}`, label: '→' }));
|
||||
});
|
||||
} else if (svcCap.length > 0) {
|
||||
svcCap.forEach((svc) => tEdges.push({ id: `e-${clusterId}-service-${svc.id}`, source: clusterId, target: `service-${svc.id}`, label: 'expose' }));
|
||||
}
|
||||
|
||||
// Layer 3: Deployment
|
||||
const depCap = deployments.slice(0, MAX_PER_LAYER);
|
||||
const depXs = distributeX(depCap.length);
|
||||
depCap.forEach((dep, i) => {
|
||||
const id = `deploy-${dep.id}`;
|
||||
tNodes.push({ id, name: dep.name, type: 'app', x: depXs[i], y: layerY[3], data: { kind: 'deployment', namespace: dep.namespace, replicas: dep.replicas, ready_replicas: dep.ready_replicas, available_replicas: dep.available_replicas, image: dep.image } });
|
||||
});
|
||||
|
||||
// Service → Deployment edges
|
||||
if (svcCap.length > 0 && depCap.length > 0) {
|
||||
svcCap.forEach((svc) => {
|
||||
const svcNodeId = `service-${svc.id}`;
|
||||
let matched: any[] = [];
|
||||
try {
|
||||
const selector = typeof svc.selector === 'string' ? JSON.parse(svc.selector || '{}') : (svc.selector || {});
|
||||
const selectorEntries = Object.entries(selector);
|
||||
if (selectorEntries.length > 0) {
|
||||
matched = depCap.filter((d) => {
|
||||
let labels: Record<string, string> = {};
|
||||
try { labels = typeof d.labels === 'string' ? JSON.parse(d.labels || '{}') : (d.labels || {}); } catch { /* */ }
|
||||
return selectorEntries.every(([k, v]) => labels[k] === v);
|
||||
});
|
||||
}
|
||||
} catch { /* */ }
|
||||
if (matched.length === 0) matched = depCap.filter((d) => d.name === svc.name);
|
||||
if (matched.length === 0) matched = depCap.filter((d) => d.namespace === svc.namespace);
|
||||
if (matched.length === 0) matched = [depCap[0]];
|
||||
matched.slice(0, 4).forEach((dep) => tEdges.push({ id: `e-${svcNodeId}-deploy-${dep.id}`, source: svcNodeId, target: `deploy-${dep.id}`, label: 'select' }));
|
||||
});
|
||||
}
|
||||
|
||||
// Layer 4: Node
|
||||
const nodeCap = nodes.slice(0, MAX_PER_LAYER);
|
||||
const nodeXs = distributeX(nodeCap.length);
|
||||
nodeCap.forEach((n, i) => {
|
||||
const id = `k8snode-${n.id}`;
|
||||
tNodes.push({ id, name: n.name, type: 'server', x: nodeXs[i], y: layerY[4], data: { kind: 'node', ip: n.ip, role: n.role, status: n.status, os: n.os, cpu: n.cpu, memory: n.memory, cpu_usage: n.cpu_usage, memory_usage: n.memory_usage, pod_count: n.pod_count } });
|
||||
});
|
||||
|
||||
// Deployment → Node edges
|
||||
const workerNodes = nodeCap.filter((n) => (n.role || '').toLowerCase() === 'worker');
|
||||
const schedulableNodes = workerNodes.length > 0 ? workerNodes : nodeCap;
|
||||
if (depCap.length > 0 && schedulableNodes.length > 0) {
|
||||
depCap.forEach((dep) => {
|
||||
const depNodeId = `deploy-${dep.id}`;
|
||||
schedulableNodes.slice(0, 3).forEach((n) => tEdges.push({ id: `e-${depNodeId}-k8snode-${n.id}`, source: depNodeId, target: `k8snode-${n.id}`, label: 'schedule' }));
|
||||
});
|
||||
}
|
||||
|
||||
return { nodes: tNodes, edges: tEdges };
|
||||
}
|
||||
|
||||
// 布局数据结构
|
||||
export interface TopologyLayoutData {
|
||||
positions: Record<string, { x: number; y: number }>;
|
||||
customNodes: TopologyNode[];
|
||||
customEdges: TopologyEdge[];
|
||||
}
|
||||
Reference in new issue
Block a user