Files
aiops/backend/internal/api/handlers/k8s_node.go
T
attacker 4dd5e10b46 refactor(k8s): Q2 kubernetes.go 模块化拆分 — 4204行单文件→12个域文件
将 handlers/kubernetes.go (4204行/134函数) 按业务域拆分为:
- kubernetes.go (106L): struct + constructor + 共享helpers
- k8s_cluster.go (642L): 集群CRUD + 同步 + RBAC
- k8s_workload.go (696L): Deployment/STS/DS/ReplicaSet操作
- k8s_pod.go (653L): Pod日志/终端/exec/指标
- k8s_pod_files.go (206L): Pod文件浏览/上传/下载
- k8s_resource.go (351L): Service/Ingress/ConfigMap/Secret/PVC/Topology
- k8s_hpa_cronjob.go (384L): HPA + CronJob
- k8s_yaml.go (213L): YAML get/update/delete/apply
- k8s_namespace.go (305L): Namespace CRUD
- k8s_events.go (207L): 事件列表+聚合
- k8s_node.go (428L): Node详情+资源分配
- k8s_compare.go (146L): 多集群对比

验收: go build零错误, 425条路由不变, 26个K8s API端点回归测试全通过
2026-06-25 04:51:21 +08:00

429 lines
12 KiB
Go

package handlers
import (
"fmt"
"net/http"
"strconv"
"strings"
"ezops/internal/models"
k8sclient "ezops/pkg/kubernetesclient"
"github.com/gin-gonic/gin"
)
func (h *KubernetesHandler) ListNodes(c *gin.Context) {
clusterID, _ := strconv.Atoi(c.Param("id"))
client, _, err := h.getK8sClient(clusterID)
if err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
return
}
nodes, err := client.ListNodes()
if err != nil {
c.JSON(http.StatusInternalServerError, models.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_mb"`
MaxPods int `json:"max_pods"`
// Metrics (optional, omitted if empty)
CPUUsageM string `json:"cpu_usage_m,omitempty"` // e.g. "122m"
MemUsageMi string `json:"mem_usage_mi,omitempty"` // e.g. "2105Mi"
}
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
}
}
s := NodeSummary{
Name: n.Metadata.Name,
IP: ip,
Role: role,
Status: nodeStatus,
OS: n.Status.NodeInfo.OSImage,
Kubelet: n.Status.NodeInfo.KubeletVersion,
CPU: parseIntResource(n.Status.Capacity["cpu"]),
Memory: parseMemResource(n.Status.Capacity["memory"]),
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"])
}
result = append(result, s)
}
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列表
// @Tags K8s集群
// @Produce json
// @Security Bearer
// @Param id path int true "集群ID"
// @Param namespace query string false "命名空间"
// @Success 200 {object} models.Response{data=[]models.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, models.Response{Code: 500, Message: "cluster not found"}); return }
client, _, err := h.getK8sClient(clusterID)
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
ns := c.Query("namespace")
deps, err := client.ListDeployments()
if err != nil { c.JSON(500, models.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, models.Response{Code: 200, Data: result})
}
// ============================================================================
// YAML Apply (kubectl apply -f)
// ============================================================================
// ApplyYAML applies a YAML manifest to create or update a resource