package service import ( "context" "encoding/json" "fmt" "net/http" "net/url" "strconv" "strings" "github.com/1024XEngineer/xinfra/server/internal/model" ) type ContainerServiceSummary struct { BusinessLineID uint64 `json:"businessLineId"` BusinessLineName string `json:"businessLineName"` Namespaces int `json:"namespaces"` Clusters int `json:"clusters"` Nodes int `json:"nodes"` ReadyNodes int `json:"readyNodes"` SchedulableNodes int `json:"schedulableNodes"` Workloads int `json:"workloads"` Pods int `json:"pods"` ReadyPods int `json:"readyPods"` CPUUsed float64 `json:"cpuUsed"` CPUTotal float64 `json:"cpuTotal"` CPUPercent int `json:"cpuPercent"` MemoryUsed float64 `json:"memoryUsed"` MemoryTotal float64 `json:"memoryTotal"` MemoryPercent int `json:"memoryPercent"` Errors []string `json:"errors,omitempty"` } type ContainerWorkload struct { Name string `json:"name"` Cluster string `json:"cluster"` Namespace string `json:"namespace"` Workload string `json:"workload"` Ready string `json:"ready"` ReadyPods int `json:"readyPods"` Pods int `json:"pods"` Image string `json:"image"` BusinessLine string `json:"biz"` Status string `json:"status"` StatusClass string `json:"statusClass"` } type ContainerServiceData struct { Summary ContainerServiceSummary `json:"summary"` Workloads []ContainerWorkload `json:"items"` } type ContainerBusinessLineQuery struct { BusinessLineID uint64 BusinessLineName string Namespaces []model.BusinessLineWayneNamespace } func (s *WayneRoleBindingService) ContainerServiceData(ctx context.Context, query ContainerBusinessLineQuery) (*ContainerServiceData, error) { summary := ContainerServiceSummary{ BusinessLineID: query.BusinessLineID, BusinessLineName: query.BusinessLineName, Namespaces: len(query.Namespaces), } workloads := make([]ContainerWorkload, 0) clusterNodes := map[string]wayneNodeSummary{} seenWorkloads := map[string]struct{}{} errors := make([]string, 0) for _, binding := range query.Namespaces { namespaceDetail, err := s.getWayneNamespace(ctx, binding.WayneNamespaceID) if err != nil { errors = append(errors, fmt.Sprintf("namespace %d: %v", binding.WayneNamespaceID, err)) continue } kubeNamespace := strings.TrimSpace(binding.KubeNamespace) if kubeNamespace == "" { kubeNamespace = namespaceDetail.KubeNamespace } clusters := namespaceDetail.Clusters() if len(clusters) == 0 { errors = append(errors, fmt.Sprintf("namespace %d has no clusterMeta", binding.WayneNamespaceID)) continue } apps, err := s.listWayneNamespaceApps(ctx, binding.WayneNamespaceID) if err != nil { errors = append(errors, fmt.Sprintf("namespace %d apps: %v", binding.WayneNamespaceID, err)) continue } if len(apps) == 0 { errors = append(errors, fmt.Sprintf("namespace %d has no apps for deployment query", binding.WayneNamespaceID)) continue } appID := apps[0].ID for _, cluster := range clusters { if _, ok := clusterNodes[cluster]; !ok { nodes, err := s.getWayneNodes(ctx, cluster) if err != nil { errors = append(errors, fmt.Sprintf("cluster %s nodes: %v", cluster, err)) } else { clusterNodes[cluster] = nodes } } items, err := s.listWayneDeployments(ctx, appID, kubeNamespace, cluster) if err != nil { errors = append(errors, fmt.Sprintf("namespace %d cluster %s app %d deployments: %v", binding.WayneNamespaceID, cluster, appID, err)) continue } for _, item := range items { workload := containerWorkloadFromMap(item, cluster, kubeNamespace, query.BusinessLineName) if workload.Name == "" { continue } key := strings.Join([]string{workload.Cluster, workload.Namespace, workload.Workload, workload.Name}, "\x00") if _, ok := seenWorkloads[key]; ok { continue } seenWorkloads[key] = struct{}{} workloads = append(workloads, workload) summary.Pods += workload.Pods summary.ReadyPods += workload.ReadyPods } } } for _, nodes := range clusterNodes { summary.Nodes += nodes.Total summary.ReadyNodes += nodes.Ready summary.SchedulableNodes += nodes.Schedulable summary.CPUUsed += nodes.CPUUsed summary.CPUTotal += nodes.CPUTotal summary.MemoryUsed += nodes.MemoryUsed summary.MemoryTotal += nodes.MemoryTotal } summary.Clusters = len(clusterNodes) summary.Workloads = len(workloads) summary.CPUPercent = ratioPercent(summary.CPUUsed, summary.CPUTotal) summary.MemoryPercent = ratioPercent(summary.MemoryUsed, summary.MemoryTotal) summary.Errors = errors return &ContainerServiceData{Summary: summary, Workloads: workloads}, nil } func (s *WayneRoleBindingService) getWayneNamespace(ctx context.Context, id uint64) (wayneNamespaceDetail, error) { result, err := s.callRaw(ctx, http.MethodGet, fmt.Sprintf("/api/v1/namespaces/%d", id), nil) if err != nil { return wayneNamespaceDetail{}, err } return parseWayneNamespaceDetail(result.Body) } func (s *WayneRoleBindingService) listWayneNamespaceApps(ctx context.Context, namespaceID uint64) ([]wayneApp, error) { values := url.Values{} values.Set("pageNo", "1") values.Set("pageSize", "500") result, err := s.callRaw(ctx, http.MethodGet, fmt.Sprintf("/api/v1/namespaces/%d/apps?%s", namespaceID, values.Encode()), nil) if err != nil { return nil, err } return parseWayneApps(result.Body) } func (s *WayneRoleBindingService) getWayneNodes(ctx context.Context, cluster string) (wayneNodeSummary, error) { result, err := s.callRaw(ctx, http.MethodGet, fmt.Sprintf("/api/v1/kubernetes/nodes/clusters/%s", url.PathEscape(cluster)), nil) if err != nil { return wayneNodeSummary{}, err } return parseWayneNodeSummary(result.Body) } func (s *WayneRoleBindingService) listWayneDeployments(ctx context.Context, appID uint64, namespace string, cluster string) ([]map[string]any, error) { values := url.Values{} values.Set("pageNo", "1") values.Set("pageSize", "500") path := fmt.Sprintf( "/api/v1/kubernetes/apps/%d/deployments/namespaces/%s/clusters/%s?%s", appID, url.PathEscape(namespace), url.PathEscape(cluster), values.Encode(), ) result, err := s.callRaw(ctx, http.MethodGet, path, nil) if err != nil { return nil, err } return parseWayneMapList(result.Body) } type wayneNamespaceDetail struct { ID uint64 `json:"id"` Name string `json:"name"` KubeNamespace string `json:"kubeNamespace"` MetaData string `json:"metaData"` } func (n wayneNamespaceDetail) Clusters() []string { var meta struct { ClusterMeta map[string]json.RawMessage `json:"clusterMeta"` } if err := json.Unmarshal([]byte(n.MetaData), &meta); err != nil { return nil } clusters := make([]string, 0, len(meta.ClusterMeta)) for cluster := range meta.ClusterMeta { if strings.TrimSpace(cluster) != "" { clusters = append(clusters, cluster) } } return clusters } type wayneApp struct { ID uint64 `json:"id"` Name string `json:"name"` } type wayneNodeSummary struct { Total int Ready int Schedulable int CPUUsed float64 CPUTotal float64 MemoryUsed float64 MemoryTotal float64 } func parseWayneNamespaceDetail(body []byte) (wayneNamespaceDetail, error) { var wrapped struct { Data wayneNamespaceDetail `json:"data"` } if err := json.Unmarshal(body, &wrapped); err != nil { return wayneNamespaceDetail{}, err } return wrapped.Data, nil } func parseWayneApps(body []byte) ([]wayneApp, error) { var wrapped struct { Data struct { List []wayneApp `json:"list"` } `json:"data"` } if err := json.Unmarshal(body, &wrapped); err != nil { return nil, err } return wrapped.Data.List, nil } func parseWayneMapList(body []byte) ([]map[string]any, error) { var wrapped struct { Data struct { List []map[string]any `json:"list"` } `json:"data"` } if err := json.Unmarshal(body, &wrapped); err != nil { return nil, err } return wrapped.Data.List, nil } func parseWayneNodeSummary(body []byte) (wayneNodeSummary, error) { var wrapped struct { Data struct { NodeSummary struct { Total int `json:"total"` Ready int `json:"ready"` Schedulable int `json:"schedulable"` } `json:"nodeSummary"` CPUSummary struct { Total float64 `json:"total"` Used float64 `json:"used"` } `json:"cpuSummary"` MemorySummary struct { Total float64 `json:"total"` Used float64 `json:"used"` } `json:"memorySummary"` } `json:"data"` } if err := json.Unmarshal(body, &wrapped); err != nil { return wayneNodeSummary{}, err } return wayneNodeSummary{ Total: wrapped.Data.NodeSummary.Total, Ready: wrapped.Data.NodeSummary.Ready, Schedulable: wrapped.Data.NodeSummary.Schedulable, CPUUsed: wrapped.Data.CPUSummary.Used, CPUTotal: wrapped.Data.CPUSummary.Total, MemoryUsed: wrapped.Data.MemorySummary.Used, MemoryTotal: wrapped.Data.MemorySummary.Total, }, nil } func containerWorkloadFromMap(item map[string]any, cluster string, namespace string, businessLineName string) ContainerWorkload { name := firstString(item, "name", "objectMeta.name", "metadata.name") ready := firstInt(item, "pods.current", "readyReplicas", "status.readyReplicas", "availableReplicas", "status.availableReplicas") total := firstInt(item, "pods.desired", "replicas", "status.replicas", "desiredReplicas", "spec.replicas") if total == 0 && ready > 0 { total = ready } status := "健康" statusClass := "ok" if total == 0 { status = "未就绪" statusClass = "warn" } else if ready < total { status = "部分异常" statusClass = "warn" } return ContainerWorkload{ Name: name, Cluster: cluster, Namespace: namespace, Workload: "Deployment", Ready: strconv.Itoa(ready) + " / " + strconv.Itoa(total), ReadyPods: ready, Pods: total, Image: firstImage(item), BusinessLine: businessLineName, Status: status, StatusClass: statusClass, } } func firstImage(item map[string]any) string { if value := firstString(item, "image", "containerImage"); value != "" { return value } for _, path := range []string{"containers", "images", "containerImages"} { value, ok := nestedValue(item, path) if !ok { continue } if images, ok := value.([]any); ok && len(images) > 0 { if image, ok := images[0].(string); ok { return image } } } if containers, ok := nestedValue(item, "spec.template.spec.containers"); ok { if rows, ok := containers.([]any); ok && len(rows) > 0 { if row, ok := rows[0].(map[string]any); ok { return stringFromAny(row["image"]) } } } return "" } func firstString(item map[string]any, paths ...string) string { for _, path := range paths { value, ok := nestedValue(item, path) if !ok { continue } if text := strings.TrimSpace(stringFromAny(value)); text != "" { return text } } return "" } func firstInt(item map[string]any, paths ...string) int { for _, path := range paths { value, ok := nestedValue(item, path) if !ok { continue } switch typed := value.(type) { case float64: return int(typed) case int: return typed case json.Number: intValue, _ := typed.Int64() return int(intValue) case string: intValue, _ := strconv.Atoi(strings.TrimSpace(typed)) return intValue } } return 0 } func nestedValue(item map[string]any, path string) (any, bool) { current := any(item) for _, part := range strings.Split(path, ".") { row, ok := current.(map[string]any) if !ok { return nil, false } current, ok = row[part] if !ok { return nil, false } } return current, true } func stringFromAny(value any) string { switch typed := value.(type) { case string: return typed case fmt.Stringer: return typed.String() default: if value == nil { return "" } return fmt.Sprintf("%v", value) } } func ratioPercent(used, total float64) int { if total <= 0 { return 0 } return int(used * 100 / total) }