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:
attacker committed 2026-06-23 17:57:09 +08:00
1 parent 28dfc519e1
commit 87b7755094
16 files changed
+921 -52

No files matched your search

+307 -18
View File
@@ -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")
+4
View File
@@ -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)