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() {