package controller import ( "fmt" "net/http" "strconv" "strings" "ezops/model" k8sclient "ezops/pkg/kubernetesclient" "github.com/gin-gonic/gin" ) func (h *KubernetesHandler) ListNodes(c *gin.Context) { // 兼容 /kubernetes-clusters/:id/nodes (ID) 和 /kubernetes/:clusterCode/nodes (code) client, _, err := h.getK8sClientByCode(c) if err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()}) return } nodes, err := client.ListNodes() if err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}) return } // Attempt to fetch metrics (best-effort, metrics-server may not be installed) metrics := map[string]k8sclient.NodeMetricsItem{} if m, err := client.GetNodeMetrics(); err == nil { for _, item := range m.Items { metrics[item.Metadata.Name] = item } } type NodeSummary struct { Name string `json:"name"` IP string `json:"ip"` Role string `json:"role"` Status string `json:"status"` OS string `json:"os"` Kubelet string `json:"kubelet_version"` CPU int `json:"cpu"` Memory int64 `json:"memory"` // MiB MaxPods int `json:"max_pods"` // Metrics percent (for list usage bars) CPUUsage float64 `json:"cpu_usage,omitempty"` MemoryUsage float64 `json:"memory_usage,omitempty"` // Metrics absolute (for detail pages) CPUUsageM string `json:"cpu_usage_m,omitempty"` // e.g. "122m" MemUsageMi string `json:"mem_usage_mi,omitempty"` // e.g. "2105Mi" // Request 占用(分配率 = 该节点 Pod 已申请 / 可分配)// Logan.Li PodCount int `json:"pod_count"` CPURequestM int `json:"cpu_request_m"` MemRequestMi int64 `json:"mem_request_mi"` CPUAllocPct float64 `json:"cpu_alloc_pct"` MemAllocPct float64 `json:"mem_alloc_pct"` } // 一次性拉取 Pod 并按节点聚合 Request:列表页展示"Request 占用"所需, // 避免每行单独查一次 Pod 列表。 // Logan.Li type reqAgg struct { cpuM int memMi int64 pods int } podReqs := map[string]*reqAgg{} if pods, perr := client.ListPods(); perr == nil && pods != nil { for _, p := range pods.Items { if p.Spec.NodeName == "" { continue } agg := podReqs[p.Spec.NodeName] if agg == nil { agg = &reqAgg{} podReqs[p.Spec.NodeName] = agg } agg.pods++ for _, ctr := range p.Spec.Containers { if ctr.Resources.Requests == nil { continue } agg.cpuM += parseCpuToMilli(ctr.Resources.Requests["cpu"]) agg.memMi += parseMemToMi(ctr.Resources.Requests["memory"]) } } } result := []NodeSummary{} for _, n := range nodes.Items { role := "worker" for k := range n.Metadata.Labels { if k == "node-role.kubernetes.io/master" || k == "node-role.kubernetes.io/control-plane" { role = "master" break } } nodeStatus := "NotReady" for _, cond := range n.Status.Conditions { if cond.Type == "Ready" && cond.Status == "True" { nodeStatus = "Ready" break } } ip := "" for _, addr := range n.Status.Addresses { if addr.Type == "InternalIP" { ip = addr.Address break } } cpuCapacity := parseIntResource(n.Status.Capacity["cpu"]) memCapacity := parseMemResource(n.Status.Capacity["memory"]) s := NodeSummary{ Name: n.Metadata.Name, IP: ip, Role: role, Status: nodeStatus, OS: n.Status.NodeInfo.OSImage, Kubelet: n.Status.NodeInfo.KubeletVersion, CPU: cpuCapacity, Memory: memCapacity, MaxPods: parseIntResource(n.Status.Capacity["pods"]), } if m, ok := metrics[n.Metadata.Name]; ok { // K8s metrics API returns CPU in nanocores ("80410173n"), memory in Ki ("2154292Ki") s.CPUUsageM = formatCpuFromNano(m.Usage["cpu"]) s.MemUsageMi = formatMemFromKi(m.Usage["memory"]) // 计算百分比用于列表页使用率条 // Logan.Li if cpuCapacity > 0 { cpuMillicores := parseCpuNanoToMilli(m.Usage["cpu"]) s.CPUUsage = float64(cpuMillicores) / float64(cpuCapacity*1000) * 100 } if memCapacity > 0 { memMi := parseMemKiToMi(m.Usage["memory"]) s.MemoryUsage = float64(memMi) / float64(memCapacity) * 100 } } // Request 占用:本节点 Pod 已申请 / 可分配。 // Logan.Li if agg := podReqs[n.Metadata.Name]; agg != nil { allocCPU := parseIntResource(n.Status.Allocatable["cpu"]) allocMem := parseMemResource(n.Status.Allocatable["memory"]) s.PodCount = agg.pods s.CPURequestM = agg.cpuM s.MemRequestMi = agg.memMi if allocCPU > 0 { s.CPUAllocPct = float64(agg.cpuM) / float64(allocCPU*1000) * 100 } if allocMem > 0 { s.MemAllocPct = float64(agg.memMi) / float64(allocMem) * 100 } } result = append(result, s) } c.JSON(http.StatusOK, model.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, model.Response{Code: 400, Message: "cluster not found"}) return } nodeName := c.Param("name") if nodeName == "" { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "node name required"}) return } client, _, err := h.getK8sClient(clusterID) if err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()}) return } // 1. Get node detail node, err := client.GetNode(nodeName) if err != nil { c.JSON(http.StatusInternalServerError, model.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, model.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列表 // @Tags K8s集群 // @Produce json // @Security Bearer // @Param id path int true "集群ID" // @Param namespace query string false "命名空间" // @Success 200 {object} model.Response{data=[]model.KubernetesPod} // @Router /kubernetes-clusters/{id}/pods [get] func (h *KubernetesHandler) GetCapacityAnalysis(c *gin.Context) { clusterID := h.resolveClusterCode(c.Param("clusterCode")) if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}) return } client, _, err := h.getK8sClient(clusterID) if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}) return } ns := c.Query("namespace") deps, err := client.ListDeployments() if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}) return } type ContainerResource struct { Name string `json:"name"` CPUReq string `json:"cpu_request"` CPULim string `json:"cpu_limit"` MemReq string `json:"mem_request"` MemLim string `json:"mem_limit"` } type CapacityRow struct { Namespace string `json:"namespace"` Name string `json:"name"` Replicas int `json:"replicas"` Containers []ContainerResource `json:"containers"` } result := []CapacityRow{} for _, d := range deps.Items { if ns != "" && d.Metadata.Namespace != ns { continue } containers := []ContainerResource{} for _, c := range d.Spec.Template.Spec.Containers { cpuReq := c.Resources.Requests["cpu"] cpuLim := c.Resources.Limits["cpu"] memReq := c.Resources.Requests["memory"] memLim := c.Resources.Limits["memory"] containers = append(containers, ContainerResource{c.Name, cpuReq, cpuLim, memReq, memLim}) } result = append(result, CapacityRow{d.Metadata.Namespace, d.Metadata.Name, d.Spec.Replicas, containers}) } c.JSON(200, model.Response{Code: 200, Data: result}) } // ============================================================================ // YAML Apply (kubectl apply -f) // ============================================================================ // ApplyYAML applies a YAML manifest to create or update a resource