diff --git a/backend/internal/models/k8s.go b/backend/internal/models/k8s.go index 882f11e..f2e3953 100644 --- a/backend/internal/models/k8s.go +++ b/backend/internal/models/k8s.go @@ -18,9 +18,10 @@ type K8sCluster struct { APIEndpoint string `gorm:"size:255" json:"api_endpoint"` Token string `gorm:"type:text" json:"-"` KubeConfig string `gorm:"type:text" json:"-"` - NodeCount int `gorm:"default:0" json:"node_count"` - PodCount int `gorm:"default:0" json:"pod_count"` - ServiceCount int `gorm:"default:0" json:"service_count"` + NodeCount int `gorm:"default:0" json:"node_count"` + PodCount int `gorm:"default:0" json:"pod_count"` + ServiceCount int `gorm:"default:0" json:"service_count"` + DeploymentCount int `gorm:"default:0" json:"deployment_count"` CPUUsage float64 `json:"cpu_usage"` MemoryUsage float64 `json:"memory_usage"` Description string `gorm:"size:500" json:"description"` diff --git a/backend/scripts/mock.go b/backend/scripts/mock.go index b1b5fae..02ef222 100644 --- a/backend/scripts/mock.go +++ b/backend/scripts/mock.go @@ -160,10 +160,10 @@ func generateDomains(db *gorm.DB) error { func generateK8sClusters(db *gorm.DB) error { clusters := []models.K8sCluster{ - {Name: "prod-cluster", Code: "prod", Version: "v1.28.0", Status: "healthy", NodeCount: 10, PodCount: 150, ServiceCount: 30, APIEndpoint: "https://prod-api.ezops.local:6443", CPUUsage: 65.5, MemoryUsage: 72.3, Description: "生产环境 Kubernetes 集群"}, - {Name: "staging-cluster", Code: "staging", Version: "v1.28.0", Status: "healthy", NodeCount: 5, PodCount: 80, ServiceCount: 15, APIEndpoint: "https://staging-api.ezops.local:6443", CPUUsage: 45.2, MemoryUsage: 55.0, Description: "预发环境 Kubernetes 集群"}, - {Name: "dev-cluster", Code: "dev", Version: "v1.27.0", Status: "warning", NodeCount: 3, PodCount: 45, ServiceCount: 8, APIEndpoint: "https://dev-api.ezops.local:6443", CPUUsage: 80.1, MemoryUsage: 85.0, Description: "开发环境 Kubernetes 集群"}, - {Name: "test-cluster", Code: "test", Version: "v1.27.0", Status: "error", NodeCount: 2, PodCount: 20, ServiceCount: 5, APIEndpoint: "https://test-api.ezops.local:6443", CPUUsage: 30.0, MemoryUsage: 40.0, Description: "测试环境 Kubernetes 集群"}, + {Name: "prod-cluster", Code: "prod", Version: "v1.28.0", Status: "healthy", NodeCount: 10, PodCount: 150, ServiceCount: 30, DeploymentCount: 15, APIEndpoint: "https://prod-api.ezops.local:6443", CPUUsage: 65.5, MemoryUsage: 72.3, Description: "生产环境 Kubernetes 集群"}, + {Name: "staging-cluster", Code: "staging", Version: "v1.28.0", Status: "healthy", NodeCount: 5, PodCount: 80, ServiceCount: 15, DeploymentCount: 10, APIEndpoint: "https://staging-api.ezops.local:6443", CPUUsage: 45.2, MemoryUsage: 55.0, Description: "预发环境 Kubernetes 集群"}, + {Name: "dev-cluster", Code: "dev", Version: "v1.27.0", Status: "warning", NodeCount: 3, PodCount: 45, ServiceCount: 8, DeploymentCount: 8, APIEndpoint: "https://dev-api.ezops.local:6443", CPUUsage: 80.1, MemoryUsage: 85.0, Description: "开发环境 Kubernetes 集群"}, + {Name: "test-cluster", Code: "test", Version: "v1.27.0", Status: "error", NodeCount: 2, PodCount: 20, ServiceCount: 5, DeploymentCount: 5, APIEndpoint: "https://test-api.ezops.local:6443", CPUUsage: 30.0, MemoryUsage: 40.0, Description: "测试环境 Kubernetes 集群"}, } for i := range clusters { if err := db.Create(&clusters[i]).Error; err != nil { @@ -171,6 +171,7 @@ func generateK8sClusters(db *gorm.DB) error { } } for _, cluster := range clusters { + // 生成节点 for j := 0; j < cluster.NodeCount; j++ { node := models.K8sNode{ ClusterID: cluster.ID, @@ -193,6 +194,141 @@ func generateK8sClusters(db *gorm.DB) error { return err } } + + // 生成 Deployments + depImages := []string{ + "nginx:1.25.3", "redis:7.2-alpine", "mysql:8.0.35", + "kafka:3.6.0", "elasticsearch:8.11.0", "grafana/grafana:10.2.0", + "prom/prometheus:v2.48.0", "busybox:1.36", "easzlab/kubeasz:3.6.1", + } + depNames := []string{ + "nginx-deploy", "redis-cluster", "mysql-master", "mysql-slave", + "kafka-broker", "es-cluster", "grafana", "prometheus", + "filebeat", "logstash", "jaeger", "zipkin", + "frontend", "backend-api", "gateway", + } + namespaces := []string{"default", "kube-system", "monitoring", "ingress-nginx", "production"} + numDeps := cluster.DeploymentCount + if numDeps < 5 { + numDeps = 5 + rand.Intn(10) + } + for d := 0; d < numDeps && d < len(depNames); d++ { + replicas := []int{1, 2, 3, 5}[rand.Intn(4)] + available := replicas + if rand.Intn(4) == 0 { + available = replicas - 1 + } + if available < 0 { + available = 0 + } + dep := models.K8sDeployment{ + ClusterID: cluster.ID, + Namespace: namespaces[rand.Intn(len(namespaces))], + Name: depNames[d], + Replicas: replicas, + AvailableReplicas: available, + ReadyReplicas: available, + Image: depImages[rand.Intn(len(depImages))], + Strategy: "RollingUpdate", + } + if err := db.Create(&dep).Error; err != nil { + return err + } + } + + // 生成 Pods + podStatuses := []string{"Running", "Running", "Running", "Running", "Pending", "CrashLoopBackOff"} + numPods := cluster.PodCount + if numPods > 30 { + numPods = 30 + } + if numPods < 10 { + numPods = 10 + rand.Intn(15) + } + for p := 0; p < numPods; p++ { + status := podStatuses[rand.Intn(len(podStatuses))] + pod := models.K8sPod{ + ClusterID: cluster.ID, + Namespace: namespaces[rand.Intn(len(namespaces))], + Name: fmt.Sprintf("%s-pod-%d", cluster.Code, p+1), + Status: status, + NodeName: fmt.Sprintf("%s-node-%d", cluster.Code, rand.Intn(cluster.NodeCount)+1), + IP: fmt.Sprintf("10.%d.%d.%d", rand.Intn(256), rand.Intn(256), rand.Intn(256)), + RestartCount: rand.Intn(10), + Image: depImages[rand.Intn(len(depImages))], + CPUUsage: rand.Float64() * 500, + MemoryUsage: rand.Float64() * 2048, + } + if err := db.Create(&pod).Error; err != nil { + return err + } + } + + // 生成 Services + svcTypes := []string{"ClusterIP", "NodePort", "LoadBalancer"} + svcNames := []string{ + "nginx-svc", "redis-svc", "mysql-svc", "kafka-svc", + "es-svc", "grafana-svc", "prometheus-svc", + "frontend-svc", "backend-svc", "gateway-svc", + } + numSvcs := cluster.ServiceCount + if numSvcs > 10 { + numSvcs = 10 + } + if numSvcs < 5 { + numSvcs = 5 + rand.Intn(5) + } + for s := 0; s < numSvcs && s < len(svcNames); s++ { + svcType := svcTypes[rand.Intn(len(svcTypes))] + ports := "[]" + switch rand.Intn(4) { + case 0: + ports = `[{"port":80,"protocol":"TCP","targetPort":80}]` + case 1: + ports = `[{"port":443,"protocol":"TCP","targetPort":8443}]` + case 2: + ports = `[{"port":8080,"protocol":"TCP","targetPort":8080},{"port":9090,"protocol":"TCP","targetPort":9090}]` + default: + ports = `[{"port":3306,"protocol":"TCP","targetPort":3306}]` + } + svc := models.K8sService{ + ClusterID: cluster.ID, + Namespace: namespaces[rand.Intn(len(namespaces))], + Name: svcNames[s], + Type: svcType, + ClusterIP: fmt.Sprintf("10.96.%d.%d", rand.Intn(256), rand.Intn(256)), + ExternalIP: "", + Ports: ports, + Selector: fmt.Sprintf(`{"app":"%s"}`, svcNames[s]), + } + if svcType == "LoadBalancer" { + svc.ExternalIP = fmt.Sprintf("203.0.113.%d", rand.Intn(256)) + } + if svcType == "NodePort" { + svc.ExternalIP = fmt.Sprintf("%d-%d", 30000+rand.Intn(1000), 32767) + } + if err := db.Create(&svc).Error; err != nil { + return err + } + } + + // 生成 Ingresses + ingNames := []string{"main-ingress", "api-ingress", "grafana-ingress", "canary-ingress"} + numIngs := 2 + rand.Intn(3) + for ig := 0; ig < numIngs && ig < len(ingNames); ig++ { + hosts := fmt.Sprintf(`["%s.ezops.local"]`, ingNames[ig]) + ing := models.K8sIngress{ + ClusterID: cluster.ID, + Namespace: namespaces[rand.Intn(len(namespaces))], + Name: ingNames[ig], + Hosts: hosts, + Backend: fmt.Sprintf("%s-svc:80", ingNames[ig]), + TLS: rand.Intn(2) == 0, + } + if err := db.Create(&ing).Error; err != nil { + return err + } + } } fmt.Println("Generated K8s clusters:", len(clusters)) return nil diff --git a/backend/scripts/seed_k8s_resources.go b/backend/scripts/seed_k8s_resources.go new file mode 100644 index 0000000..93dff51 --- /dev/null +++ b/backend/scripts/seed_k8s_resources.go @@ -0,0 +1,176 @@ +// Standalone script: seed K8s Pods/Deployments/Services/Ingresses for existing clusters. +// Usage: go run scripts/seed_k8s_resources.go +//go:build ignore + +package main + +import ( + "fmt" + "math/rand" + "time" + + "ezops/internal/config" + "ezops/internal/models" + "ezops/pkg/database" +) + +func main() { + cfg := config.Load() + db, err := database.New(cfg.Database) + if err != nil { + fmt.Printf("DB connection failed: %v\n", err) + return + } + rand.Seed(time.Now().UnixNano()) + + var clusters []models.K8sCluster + db.Find(&clusters) + if len(clusters) == 0 { + fmt.Println("No clusters found — run mock.go first") + return + } + + depImages := []string{ + "nginx:1.25.3", "redis:7.2-alpine", "mysql:8.0.35", + "kafka:3.6.0", "elasticsearch:8.11.0", "grafana/grafana:10.2.0", + "prom/prometheus:v2.48.0", "busybox:1.36", "easzlab/kubeasz:3.6.1", + } + depNames := []string{ + "nginx-deploy", "redis-cluster", "mysql-master", "mysql-slave", + "kafka-broker", "es-cluster", "grafana", "prometheus", + "filebeat", "logstash", "jaeger", "zipkin", + "frontend", "backend-api", "gateway", + } + namespaces := []string{"default", "kube-system", "monitoring", "ingress-nginx", "production"} + svcTypes := []string{"ClusterIP", "NodePort", "LoadBalancer"} + svcNames := []string{ + "nginx-svc", "redis-svc", "mysql-svc", "kafka-svc", + "es-svc", "grafana-svc", "prometheus-svc", + "frontend-svc", "backend-svc", "gateway-svc", + } + ingNames := []string{"main-ingress", "api-ingress", "grafana-ingress", "canary-ingress"} + podStatuses := []string{"Running", "Running", "Running", "Running", "Pending", "CrashLoopBackOff"} + + for _, cluster := range clusters { + // Skip if pods already exist for this cluster + var podCount int64 + db.Model(&models.K8sPod{}).Where("cluster_id = ?", cluster.ID).Count(&podCount) + if podCount > 0 { + fmt.Printf("Cluster %s already has %d pods, skipping\n", cluster.Name, podCount) + continue + } + + numNodes := cluster.NodeCount + if numNodes == 0 { + numNodes = 3 + } + + // Deployments + numDeps := 5 + rand.Intn(10) + if numDeps > len(depNames) { + numDeps = len(depNames) + } + for d := 0; d < numDeps; d++ { + replicas := []int{1, 2, 3, 5}[rand.Intn(4)] + available := replicas + if rand.Intn(4) == 0 { + available = replicas - 1 + } + if available < 0 { + available = 0 + } + db.Create(&models.K8sDeployment{ + ClusterID: cluster.ID, + Namespace: namespaces[rand.Intn(len(namespaces))], + Name: depNames[d], + Replicas: replicas, + AvailableReplicas: available, + ReadyReplicas: available, + Image: depImages[rand.Intn(len(depImages))], + Strategy: "RollingUpdate", + }) + } + + // Pods + numPods := 12 + rand.Intn(18) + for p := 0; p < numPods; p++ { + db.Create(&models.K8sPod{ + ClusterID: cluster.ID, + Namespace: namespaces[rand.Intn(len(namespaces))], + Name: fmt.Sprintf("%s-pod-%d", cluster.Code, p+1), + Status: podStatuses[rand.Intn(len(podStatuses))], + NodeName: fmt.Sprintf("%s-node-%d", cluster.Code, rand.Intn(numNodes)+1), + IP: fmt.Sprintf("10.%d.%d.%d", rand.Intn(256), rand.Intn(256), rand.Intn(256)), + RestartCount: rand.Intn(10), + Image: depImages[rand.Intn(len(depImages))], + CPUUsage: rand.Float64() * 500, + MemoryUsage: rand.Float64() * 2048, + }) + } + + // Services + numSvcs := 5 + rand.Intn(5) + if numSvcs > len(svcNames) { + numSvcs = len(svcNames) + } + for s := 0; s < numSvcs; s++ { + svcType := svcTypes[rand.Intn(len(svcTypes))] + ports := "[]" + switch rand.Intn(4) { + case 0: + ports = `[{"port":80,"protocol":"TCP","targetPort":80}]` + case 1: + ports = `[{"port":443,"protocol":"TCP","targetPort":8443}]` + case 2: + ports = `[{"port":8080,"protocol":"TCP","targetPort":8080},{"port":9090,"protocol":"TCP","targetPort":9090}]` + default: + ports = `[{"port":3306,"protocol":"TCP","targetPort":3306}]` + } + svc := models.K8sService{ + ClusterID: cluster.ID, + Namespace: namespaces[rand.Intn(len(namespaces))], + Name: svcNames[s], + Type: svcType, + ClusterIP: fmt.Sprintf("10.96.%d.%d", rand.Intn(256), rand.Intn(256)), + Ports: ports, + Selector: fmt.Sprintf(`{"app":"%s"}`, svcNames[s]), + } + if svcType == "LoadBalancer" { + svc.ExternalIP = fmt.Sprintf("203.0.113.%d", rand.Intn(256)) + } + if svcType == "NodePort" { + svc.ExternalIP = fmt.Sprintf("%d-%d", 30000+rand.Intn(1000), 32767) + } + db.Create(&svc) + } + + // Ingresses + numIngs := 2 + rand.Intn(3) + if numIngs > len(ingNames) { + numIngs = len(ingNames) + } + for ig := 0; ig < numIngs; ig++ { + db.Create(&models.K8sIngress{ + ClusterID: cluster.ID, + Namespace: namespaces[rand.Intn(len(namespaces))], + Name: ingNames[ig], + Hosts: fmt.Sprintf(`["%s.ezops.local"]`, ingNames[ig]), + Backend: fmt.Sprintf("%s-svc:80", ingNames[ig]), + TLS: rand.Intn(2) == 0, + }) + } + + // Update cluster counts to match actual data + var depCnt, podCnt, svcCnt int64 + db.Model(&models.K8sDeployment{}).Where("cluster_id = ?", cluster.ID).Count(&depCnt) + db.Model(&models.K8sPod{}).Where("cluster_id = ?", cluster.ID).Count(&podCnt) + db.Model(&models.K8sService{}).Where("cluster_id = ?", cluster.ID).Count(&svcCnt) + db.Model(&cluster).Updates(map[string]interface{}{ + "deployment_count": depCnt, + }) + + fmt.Printf("Cluster %s: +%d deployments, +%d pods, +%d services, +%d ingresses\n", + cluster.Name, numDeps, numPods, numSvcs, numIngs) + } + fmt.Println("K8s resources seeded successfully!") +} diff --git a/frontend/src/components/k8s/K8sClusters.tsx b/frontend/src/components/k8s/K8sClusters.tsx index 6932ea4..c8d59cd 100644 --- a/frontend/src/components/k8s/K8sClusters.tsx +++ b/frontend/src/components/k8s/K8sClusters.tsx @@ -109,18 +109,20 @@ export function K8sClusters() { const loadClusterDetail = async (clusterId: string) => { try { + // 后端 ListNodes/ListPods/etc 返回 data 直接为数组(非 {list,total}) const [nodesRes, podsRes, depRes, svcRes, ingRes] = await Promise.all([ - k8sApi.getNodes(clusterId).catch(() => ({ list: [] })), - k8sApi.getPods(clusterId).catch(() => ({ list: [] })), - k8sApi.getDeployments(clusterId).catch(() => ({ list: [] })), - k8sApi.getServices(clusterId).catch(() => ({ list: [] })), - k8sApi.getIngresses(clusterId).catch(() => ({ list: [] })), + k8sApi.getNodes(clusterId).catch(() => []), + k8sApi.getPods(clusterId).catch(() => []), + k8sApi.getDeployments(clusterId).catch(() => []), + k8sApi.getServices(clusterId).catch(() => []), + k8sApi.getIngresses(clusterId).catch(() => []), ]); - setNodes((nodesRes as any)?.list || []); - setPods((podsRes as any)?.list || []); - setDeployments((depRes as any)?.list || []); - setServices((svcRes as any)?.list || []); - setIngresses((ingRes as any)?.list || []); + const toArray = (v: any) => (Array.isArray(v) ? v : (v?.list || [])); + setNodes(toArray(nodesRes)); + setPods(toArray(podsRes)); + setDeployments(toArray(depRes)); + setServices(toArray(svcRes)); + setIngresses(toArray(ingRes)); } catch (e) { console.error('加载集群详情失败:', e); } @@ -683,12 +685,12 @@ export function K8sClusters() { - {node.status === 'ready' ? 'Ready' : 'NotReady'} + {(node.status || '').toLowerCase() === 'ready' ? 'Ready' : 'NotReady'} @@ -739,9 +741,9 @@ export function K8sClusters() { - {pod.restarts ?? 0} - {pod.cpu} - {pod.memory} - {pod.node} + {pod.restart_count ?? 0} + {Number(pod.cpu_usage || 0).toFixed(1)}m + {Number(pod.memory_usage || 0).toFixed(0)}Mi + {pod.node_name} )) )}