feat(k8s): M1 SyncCluster 增加 metrics 采集

后端:
- SyncCluster 中调 GetNodeMetrics() 获取 metrics-server 数据
- 节点 INSERT 增加 cpu_usage/memory_usage 百分比字段
- 新增 parseCpuNanoToMilli / parseMemKiToMi 辅助函数
- 采集 Pod metrics(为 M2 准备)

前端:
- KubernetesClusterDetail 删除硬编码 resourceData mock
- 改为从节点 metrics 实时计算集群 CPU/内存使用率
- 图表 Y 轴固定 0-100 域,底部显示百分比数值

新增文档:
- ROADMAP.md / TDD_PLAN.md / PROJECT_EVALUATION.md
This commit is contained in:
attacker committed 2026-06-21 21:45:42 +08:00
1 parent 3b112ee813
commit b3d2011243
5 files changed
+289 -729

No files matched your search

+54 -2
View File
@@ -647,6 +647,24 @@ func (h *KubernetesHandler) SyncCluster(c *gin.Context) {
result := k8sclient.SyncResult{}
// Fetch node metrics (best-effort — metrics-server may not be installed) // Logan.Li
nodeMetrics := map[string]k8sclient.NodeMetricsItem{}
if m, err := client.GetNodeMetrics(); err == nil {
for _, item := range m.Items {
nodeMetrics[item.Metadata.Name] = item
}
} else {
log.Printf("[K8s] sync: GetNodeMetrics failed (metrics-server not installed?): %v", err)
}
// Fetch pod metrics (best-effort) // Logan.Li
podMetrics := map[string]k8sclient.PodMetricsItem{} // key: "namespace/podname"
if pm, err := client.ListPodMetrics(""); err == nil {
for _, item := range pm.Items {
podMetrics[item.Metadata.Namespace+"/"+item.Metadata.Name] = item
}
}
// Sync nodes
if nodes, err := client.ListNodes(); err == nil {
// Use raw SQL for reliability (avoid GORM soft-delete / auto-timestamp issues)
@@ -674,10 +692,24 @@ func (h *KubernetesHandler) SyncCluster(c *gin.Context) {
}
labelsJSON, _ := json.Marshal(n.Metadata.Labels)
now := time.Now()
err2 := h.db.Exec(`INSERT INTO kubernetes_nodes (cluster_id, name, ip, role, status, os, kubelet_version, cpu, memory, max_pods, labels, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
// Compute CPU/memory usage percentage from metrics-server data // Logan.Li
cpuUsagePct, memUsagePct := 0.0, 0.0
if m, ok := nodeMetrics[n.Metadata.Name]; ok {
cpuCores := parseIntResource(n.Status.Capacity["cpu"])
memMB := parseMemResource(n.Status.Capacity["memory"])
if cpuCores > 0 {
cpuMillicores := parseCpuNanoToMilli(m.Usage["cpu"])
cpuUsagePct = float64(cpuMillicores) / float64(cpuCores*1000) * 100
}
if memMB > 0 {
memMi := parseMemKiToMi(m.Usage["memory"])
memUsagePct = float64(memMi) / float64(memMB) * 100
}
}
err2 := h.db.Exec(`INSERT INTO kubernetes_nodes (cluster_id, name, ip, role, status, os, kubelet_version, cpu, memory, max_pods, cpu_usage, memory_usage, labels, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
cluster.ID, n.Metadata.Name, ip, role, nodeStatus, n.Status.NodeInfo.OSImage, n.Status.NodeInfo.KubeletVersion,
parseIntResource(n.Status.Capacity["cpu"]), parseMemResource(n.Status.Capacity["memory"]), parseIntResource(n.Status.Capacity["pods"]),
string(labelsJSON), now, now).Error
cpuUsagePct, memUsagePct, string(labelsJSON), now, now).Error
if err2 != nil {
log.Printf("[K8s] insert node %s error: %v", n.Metadata.Name, err2)
} else {
@@ -1928,6 +1960,17 @@ func formatCpuFromNano(raw string) string {
return fmt.Sprintf("%dm", milli)
}
// parseCpuNanoToMilli converts K8s metrics CPU (nanocores "100354992n") to millicores (int) // Logan.Li
func parseCpuNanoToMilli(raw string) int {
if raw == "" { return 0 }
val := strings.TrimSuffix(raw, "n")
var nano int64
fmt.Sscanf(val, "%d", &nano)
milli := nano / 1000000
if milli < 1 { milli = 1 }
return int(milli)
}
// formatMemFromKi converts K8s metrics memory (Ki "2154292Ki") to Mi "2105Mi"
func formatMemFromKi(raw string) string {
if raw == "" { return "" }
@@ -1939,6 +1982,15 @@ func formatMemFromKi(raw string) string {
return fmt.Sprintf("%dMi", mi)
}
// parseMemKiToMi converts K8s metrics memory (Ki "2154292Ki") to Mi (int) // Logan.Li
func parseMemKiToMi(raw string) int {
if raw == "" { return 0 }
val := strings.TrimSuffix(raw, "Ki")
var ki int64
fmt.Sscanf(val, "%d", &ki)
return int(ki / 1024)
}
// GetResourceYAMLNS returns the YAML of a K8s resource
func (h *KubernetesHandler) GetResourceYAMLNS(c *gin.Context) {