feat(k8s): M10 Node 详情页 — PID/容器运行时/分配率
- 后端: GetNodeDetail handler — 获取单个节点完整信息 * 系统信息: container_runtime/architecture/kubelet/os/kernel * 资源分配率: 遍历 Pod 计算 CPU/Memory/Pods requested vs allocatable * metrics-server 数据注入 (cpu_usage_m/mem_usage_mi) * 全量 conditions/taints/labels + 节点上的 Pod 列表 - 后端: K8s 客户端扩展 * NodeTaint/NodeCondition 扩展字段 (reason/message) * NodeInfo 增加 architecture/operatingSystem * GetNode(name) 单节点查询方法 * parseCpuToMilli()/parseMemToMi() 资源解析辅助函数 - 后端: 注册路由 * GET /kubernetes/:clusterCode/nodes/:name (Kuboard code) * GET /kubernetes-clusters/:id/nodes/:name (ID) - 前端: NodeDetailPage.tsx (~330行) * 系统信息卡片 + 资源分配率卡片(进度条) * 条件卡片 + 污点卡片 + 标签卡片 + Pod 列表 - 前端: 节点名称可点击跳转 (ClusterOverviewPage) - 验证: API/SPA/TS/Vite 全部通过
This commit is contained in:
1 parent
28dfc519e1
commit
87b7755094
16 files changed
+921
-52
No files matched your search
@@ -0,0 +1,29 @@
|
||||
# AIOps Backend Configuration (External MySQL/Redis)
|
||||
mode: release
|
||||
port: "8080"
|
||||
log_level: info
|
||||
|
||||
# MySQL 数据库 (连接 75 的 MySQL)
|
||||
database:
|
||||
driver: mysql
|
||||
host: 10.10.10.75
|
||||
port: 3306
|
||||
user: aiops
|
||||
password: aiops_pass_2026
|
||||
dbname: aiops
|
||||
charset: utf8mb4
|
||||
max_conn: 100
|
||||
max_idle: 10
|
||||
|
||||
# Redis 缓存 (连接 75 的 Redis)
|
||||
redis:
|
||||
driver: redis
|
||||
host: 10.10.10.75
|
||||
port: 6379
|
||||
password: ""
|
||||
db: 0
|
||||
|
||||
# JWT 配置
|
||||
jwt:
|
||||
secret: "aiops-jwt-secret-key-2026"
|
||||
expire_hour: 24
|
||||
@@ -241,22 +241,27 @@ func (h *KubernetesHandler) GetClusterDetail(c *gin.Context) {
|
||||
Code: 200,
|
||||
Message: "success",
|
||||
Data: map[string]interface{}{
|
||||
"id": cluster.ID,
|
||||
"name": cluster.Name,
|
||||
"code": cluster.Code,
|
||||
"version": cluster.Version,
|
||||
"status": cluster.Status,
|
||||
"config_type": cluster.ConfigType,
|
||||
"api_server": cluster.APIServer,
|
||||
"api_endpoint": cluster.APIEndpoint,
|
||||
"description": cluster.Description,
|
||||
"has_token": cluster.Token != "",
|
||||
"id": cluster.ID,
|
||||
"name": cluster.Name,
|
||||
"code": cluster.Code,
|
||||
"version": cluster.Version,
|
||||
"status": cluster.Status,
|
||||
"config_type": cluster.ConfigType,
|
||||
"api_server": cluster.APIServer,
|
||||
"api_endpoint": cluster.APIEndpoint,
|
||||
"description": cluster.Description,
|
||||
"has_token": cluster.Token != "",
|
||||
"has_kube_config": cluster.KubeConfig != "",
|
||||
"node_count": cluster.NodeCount,
|
||||
"pod_count": cluster.PodCount,
|
||||
"labels": cluster.Labels,
|
||||
"created_at": cluster.CreatedAt,
|
||||
"updated_at": cluster.UpdatedAt,
|
||||
"node_count": cluster.NodeCount,
|
||||
"pod_count": cluster.PodCount,
|
||||
"service_count": cluster.ServiceCount,
|
||||
"deployment_count": cluster.DeploymentCount,
|
||||
"cpu_usage": cluster.CPUUsage,
|
||||
"memory_usage": cluster.MemoryUsage,
|
||||
"labels": cluster.Labels,
|
||||
"last_sync_at": cluster.LastSyncAt,
|
||||
"created_at": cluster.CreatedAt,
|
||||
"updated_at": cluster.UpdatedAt,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -376,6 +381,286 @@ func (h *KubernetesHandler) ListNodes(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: result})
|
||||
}
|
||||
|
||||
// GetNodeDetail 获取节点详情(PID/容器运行时/分配率)// M10
|
||||
// @Summary 获取节点详情
|
||||
// @Description 获取指定集群中单个节点的详细信息,包含系统信息、资源分配、污点、条件等
|
||||
// @Tags K8s集群
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Router /kubernetes/{clusterCode}/nodes/{name} [get]
|
||||
func (h *KubernetesHandler) GetNodeDetail(c *gin.Context) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 {
|
||||
code := c.Param("clusterCode")
|
||||
if code == "" {
|
||||
code = c.Param("id")
|
||||
}
|
||||
clusterID = h.resolveClusterCode(code)
|
||||
}
|
||||
if clusterID == 0 {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "cluster not found"})
|
||||
return
|
||||
}
|
||||
|
||||
nodeName := c.Param("name")
|
||||
if nodeName == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "node name required"})
|
||||
return
|
||||
}
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 1. Get node detail
|
||||
node, err := client.GetNode(nodeName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Get node metrics (best-effort)
|
||||
var cpuUsageM, memUsageMi string
|
||||
if m, err := client.GetNodeMetrics(); err == nil {
|
||||
for _, item := range m.Items {
|
||||
if item.Metadata.Name == nodeName {
|
||||
cpuUsageM = formatCpuFromNano(item.Usage["cpu"])
|
||||
memUsageMi = formatMemFromKi(item.Usage["memory"])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Get pods on this node for allocation calculation
|
||||
pods, _ := client.ListPods()
|
||||
podCount := 0
|
||||
type podBrief struct {
|
||||
Namespace string `json:"namespace"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
IP string `json:"ip"`
|
||||
CPURequestM int `json:"cpu_request_m"`
|
||||
MemRequestMi int64 `json:"mem_request_mi"`
|
||||
RestartCount int `json:"restart_count"`
|
||||
}
|
||||
var podList []podBrief
|
||||
totalCPURequestM := 0
|
||||
totalMemRequestMi := int64(0)
|
||||
for _, p := range pods.Items {
|
||||
if p.Spec.NodeName != nodeName {
|
||||
continue
|
||||
}
|
||||
podCount++
|
||||
podCPUReq := 0
|
||||
podMemReq := int64(0)
|
||||
for _, ctr := range p.Spec.Containers {
|
||||
if req := ctr.Resources.Requests; req != nil {
|
||||
podCPUReq += parseCpuToMilli(req["cpu"])
|
||||
podMemReq += parseMemToMi(req["memory"])
|
||||
}
|
||||
}
|
||||
totalCPURequestM += podCPUReq
|
||||
totalMemRequestMi += podMemReq
|
||||
|
||||
status := "Unknown"
|
||||
if p.Status.Phase != "" {
|
||||
status = p.Status.Phase
|
||||
}
|
||||
restartCount := 0
|
||||
for _, cs := range p.Status.ContainerStatuses {
|
||||
restartCount += cs.RestartCount
|
||||
}
|
||||
podList = append(podList, podBrief{
|
||||
Namespace: p.Metadata.Namespace,
|
||||
Name: p.Metadata.Name,
|
||||
Status: status,
|
||||
IP: p.Status.PodIP,
|
||||
CPURequestM: podCPUReq,
|
||||
MemRequestMi: podMemReq,
|
||||
RestartCount: restartCount,
|
||||
})
|
||||
}
|
||||
|
||||
// 4. Determine role
|
||||
role := "worker"
|
||||
for k := range node.Metadata.Labels {
|
||||
if k == "node-role.kubernetes.io/master" || k == "node-role.kubernetes.io/control-plane" {
|
||||
role = "master"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Determine status
|
||||
nodeStatus := "NotReady"
|
||||
for _, cond := range node.Status.Conditions {
|
||||
if cond.Type == "Ready" && cond.Status == "True" {
|
||||
nodeStatus = "Ready"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Get IP
|
||||
internalIP := ""
|
||||
externalIP := ""
|
||||
hostname := ""
|
||||
for _, addr := range node.Status.Addresses {
|
||||
switch addr.Type {
|
||||
case "InternalIP":
|
||||
internalIP = addr.Address
|
||||
case "ExternalIP":
|
||||
externalIP = addr.Address
|
||||
case "Hostname":
|
||||
hostname = addr.Address
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Build conditions list
|
||||
type condInfo struct {
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
conditions := []condInfo{}
|
||||
for _, cond := range node.Status.Conditions {
|
||||
conditions = append(conditions, condInfo{
|
||||
Type: cond.Type,
|
||||
Status: cond.Status,
|
||||
Reason: cond.Reason,
|
||||
Message: cond.Message,
|
||||
})
|
||||
}
|
||||
|
||||
// 8. Build taints list
|
||||
type taintInfo struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
Effect string `json:"effect"`
|
||||
}
|
||||
taints := []taintInfo{}
|
||||
for _, t := range node.Spec.Taints {
|
||||
taints = append(taints, taintInfo{
|
||||
Key: t.Key,
|
||||
Value: t.Value,
|
||||
Effect: t.Effect,
|
||||
})
|
||||
}
|
||||
|
||||
// 9. Calculate allocation rate
|
||||
capCPU := parseIntResource(node.Status.Capacity["cpu"])
|
||||
allocCPU := parseIntResource(node.Status.Allocatable["cpu"])
|
||||
capMem := parseMemResource(node.Status.Capacity["memory"])
|
||||
allocMem := parseMemResource(node.Status.Allocatable["memory"])
|
||||
capPods := parseIntResource(node.Status.Capacity["pods"])
|
||||
allocPods := parseIntResource(node.Status.Allocatable["pods"])
|
||||
|
||||
cpuReqCores := float64(totalCPURequestM) / 1000.0
|
||||
cpuAllocPct := 0.0
|
||||
if allocCPU > 0 {
|
||||
cpuAllocPct = (cpuReqCores / float64(allocCPU)) * 100
|
||||
}
|
||||
memAllocPct := 0.0
|
||||
if allocMem > 0 {
|
||||
memAllocPct = (float64(totalMemRequestMi) / float64(allocMem)) * 100
|
||||
}
|
||||
podAllocPct := 0.0
|
||||
if allocPods > 0 {
|
||||
podAllocPct = (float64(podCount) / float64(allocPods)) * 100
|
||||
}
|
||||
|
||||
// 10. Build labels list
|
||||
type labelEntry struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
labels := []labelEntry{}
|
||||
for k, v := range node.Metadata.Labels {
|
||||
labels = append(labels, labelEntry{Key: k, Value: v})
|
||||
}
|
||||
|
||||
detail := map[string]interface{}{
|
||||
"name": node.Metadata.Name,
|
||||
"role": role,
|
||||
"status": nodeStatus,
|
||||
"creation_time": node.Metadata.CreationTimestamp,
|
||||
"internal_ip": internalIP,
|
||||
"external_ip": externalIP,
|
||||
"hostname": hostname,
|
||||
"pod_cidr": node.Spec.PodCIDR,
|
||||
"container_runtime": node.Status.NodeInfo.ContainerRuntimeVersion,
|
||||
"kubelet_version": node.Status.NodeInfo.KubeletVersion,
|
||||
"os_image": node.Status.NodeInfo.OSImage,
|
||||
"kernel_version": node.Status.NodeInfo.KernelVersion,
|
||||
"architecture": node.Status.NodeInfo.Architecture,
|
||||
"operating_system": node.Status.NodeInfo.OperatingSystem,
|
||||
// Capacity & Allocatable
|
||||
"capacity": node.Status.Capacity,
|
||||
"allocatable": node.Status.Allocatable,
|
||||
// Metrics
|
||||
"cpu_usage_m": cpuUsageM,
|
||||
"mem_usage_mi": memUsageMi,
|
||||
// Allocation
|
||||
"pod_count": podCount,
|
||||
"cpu_request_m": totalCPURequestM,
|
||||
"mem_request_mi": totalMemRequestMi,
|
||||
"cpu_alloc_pct": cpuAllocPct,
|
||||
"mem_alloc_pct": memAllocPct,
|
||||
"pod_alloc_pct": podAllocPct,
|
||||
"cpu_capacity": capCPU,
|
||||
"cpu_allocatable": allocCPU,
|
||||
"mem_capacity_mb": capMem,
|
||||
"mem_allocatable_mb": allocMem,
|
||||
"pod_capacity": capPods,
|
||||
"pod_allocatable": allocPods,
|
||||
// Lists
|
||||
"conditions": conditions,
|
||||
"taints": taints,
|
||||
"labels": labels,
|
||||
"pods": podList,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: detail})
|
||||
}
|
||||
|
||||
// parseCpuToMilli converts CPU resource string to millicores (e.g. "100m"→100, "2"→2000)
|
||||
func parseCpuToMilli(val string) int {
|
||||
if val == "" {
|
||||
return 0
|
||||
}
|
||||
if strings.HasSuffix(val, "m") {
|
||||
var i int
|
||||
fmt.Sscanf(strings.TrimSuffix(val, "m"), "%d", &i)
|
||||
return i
|
||||
}
|
||||
var i int
|
||||
fmt.Sscanf(val, "%d", &i)
|
||||
return i * 1000
|
||||
}
|
||||
|
||||
// parseMemToMi converts memory resource string to Mi (e.g. "256Mi"→256, "1Gi"→1024, "512Ki"→0)
|
||||
func parseMemToMi(val string) int64 {
|
||||
if val == "" {
|
||||
return 0
|
||||
}
|
||||
var num float64
|
||||
var unit string
|
||||
fmt.Sscanf(val, "%f%s", &num, &unit)
|
||||
switch unit {
|
||||
case "Ki":
|
||||
return int64(num / 1024)
|
||||
case "Mi":
|
||||
return int64(num)
|
||||
case "Gi":
|
||||
return int64(num * 1024)
|
||||
case "Ti":
|
||||
return int64(num * 1024 * 1024)
|
||||
default:
|
||||
return int64(num / 1024 / 1024)
|
||||
}
|
||||
}
|
||||
|
||||
// ListPods 获取Pod列表
|
||||
// @Summary 获取Pod列表
|
||||
// @Description 获取指定集群的Pod列表
|
||||
@@ -936,7 +1221,6 @@ func (h *KubernetesHandler) GetPodLogs(c *gin.Context) {
|
||||
// 3 = error (server→client, JSON)
|
||||
// 4 = resize (client→server, JSON {"Height","Width"})
|
||||
func (h *KubernetesHandler) ExecTerminal(c *gin.Context) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
namespace := c.Param("namespace")
|
||||
podName := c.Param("podName")
|
||||
container := c.Query("container")
|
||||
@@ -945,7 +1229,7 @@ func (h *KubernetesHandler) ExecTerminal(c *gin.Context) {
|
||||
command = "bash"
|
||||
}
|
||||
|
||||
client, cluster, err := h.getK8sClient(clusterID)
|
||||
client, cluster, err := h.getK8sClientByCode(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
@@ -1526,7 +1810,12 @@ func (h *KubernetesHandler) resolveClusterCode(code string) int {
|
||||
func (h *KubernetesHandler) getK8sClientByCode(c *gin.Context) (*k8sclient.Client, *models.KubernetesCluster, error) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 {
|
||||
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||
// 支持 :clusterCode 或 :id 参数
|
||||
code := c.Param("clusterCode")
|
||||
if code == "" {
|
||||
code = c.Param("id")
|
||||
}
|
||||
clusterID = h.resolveClusterCode(code)
|
||||
}
|
||||
if clusterID == 0 {
|
||||
return nil, nil, fmt.Errorf("cluster not found")
|
||||
|
||||
@@ -284,6 +284,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
||||
clusters.DELETE("/:id", k8sHandler.DeleteCluster)
|
||||
clusters.POST("/:id/sync", k8sHandler.SyncCluster) // 同步集群资源 // Logan.Li
|
||||
clusters.GET("/:id/nodes", k8sHandler.ListNodes)
|
||||
clusters.GET("/:id/nodes/:name", k8sHandler.GetNodeDetail) // M10 节点详情 // Logan.Li
|
||||
clusters.GET("/:id/pods", k8sHandler.ListPods)
|
||||
clusters.GET("/:id/deployments", k8sHandler.ListDeployments)
|
||||
clusters.GET("/:id/services", k8sHandler.ListServices)
|
||||
@@ -336,6 +337,9 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
||||
k8sHandler.ListDeploymentsNS(c)
|
||||
})
|
||||
|
||||
// Node detail (cluster-scoped, not namespace-scoped) // M10
|
||||
kubernetesRouter.GET("/:clusterCode/nodes/:name", k8sHandler.GetNodeDetail)
|
||||
|
||||
// Deployments
|
||||
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/deployments", k8sHandler.ListDeploymentsNS)
|
||||
kubernetesRouter.POST("/:clusterCode/namespace/:namespace/deployments/:name/scale", k8sHandler.ScaleDeploymentNS)
|
||||
|
||||
@@ -209,7 +209,15 @@ type NodeItem struct {
|
||||
}
|
||||
|
||||
type NodeSpec struct {
|
||||
PodCIDR string `json:"podCIDR"`
|
||||
PodCIDR string `json:"podCIDR"`
|
||||
Taints []NodeTaint `json:"taints,omitempty"`
|
||||
}
|
||||
|
||||
type NodeTaint struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
Effect string `json:"effect"`
|
||||
TimeAdded string `json:"timeAdded,omitempty"`
|
||||
}
|
||||
|
||||
type NodeStatus struct {
|
||||
@@ -223,15 +231,19 @@ type NodeStatus struct {
|
||||
type ResourceList map[string]string
|
||||
|
||||
type NodeCondition struct {
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type NodeInfo struct {
|
||||
KubeletVersion string `json:"kubeletVersion"`
|
||||
OSImage string `json:"osImage"`
|
||||
KernelVersion string `json:"kernelVersion"`
|
||||
KubeletVersion string `json:"kubeletVersion"`
|
||||
OSImage string `json:"osImage"`
|
||||
KernelVersion string `json:"kernelVersion"`
|
||||
ContainerRuntimeVersion string `json:"containerRuntimeVersion"`
|
||||
Architecture string `json:"architecture"`
|
||||
OperatingSystem string `json:"operatingSystem"`
|
||||
}
|
||||
|
||||
type NodeAddress struct {
|
||||
@@ -421,6 +433,15 @@ func (c *Client) ListNodes() (*NodeList, error) {
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetNode fetches a single node by name
|
||||
func (c *Client) GetNode(name string) (*NodeItem, error) {
|
||||
var result NodeItem
|
||||
if err := c.get("/api/v1/nodes/"+name, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// ListPods fetches all pods across namespaces
|
||||
func (c *Client) ListPods() (*PodList, error) {
|
||||
var result PodList
|
||||
@@ -672,7 +693,7 @@ func (c *Client) ExecInPod(namespace, podName, container string, command []strin
|
||||
|
||||
dialer := websocket.Dialer{
|
||||
TLSClientConfig: c.TLSConfig,
|
||||
Subprotocols: []string{"channel.k8s.io"},
|
||||
Subprotocols: []string{"v4.channel.k8s.io", "v3.channel.k8s.io", "channel.k8s.io"},
|
||||
HandshakeTimeout: 10 * time.Second,
|
||||
}
|
||||
header := http.Header{}
|
||||
|
||||
Reference in new issue
Block a user