2026-07-22 14:57:11 +08:00
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/url"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
2026-07-23 18:36:24 +08:00
|
|
|
"time"
|
2026-07-22 14:57:11 +08:00
|
|
|
|
|
|
|
|
"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
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-23 18:36:24 +08:00
|
|
|
type WayneDeploymentHistory struct {
|
|
|
|
|
ID int64 `json:"id"`
|
|
|
|
|
Type int `json:"type"`
|
|
|
|
|
ResourceID int64 `json:"resourceId"`
|
|
|
|
|
ResourceName string `json:"resourceName"`
|
|
|
|
|
TemplateID int64 `json:"templateId"`
|
|
|
|
|
Cluster string `json:"cluster"`
|
|
|
|
|
Status int `json:"status"`
|
|
|
|
|
Message string `json:"message"`
|
|
|
|
|
User string `json:"user"`
|
|
|
|
|
CreateTime string `json:"createTime"`
|
|
|
|
|
CreatedAt time.Time `json:"-"`
|
|
|
|
|
BusinessLineID uint64 `json:"businessLineId"`
|
|
|
|
|
NamespaceID uint64 `json:"namespaceId"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-22 14:57:11 +08:00
|
|
|
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{}
|
2026-07-22 15:20:33 +08:00
|
|
|
seenWorkloads := map[string]struct{}{}
|
2026-07-22 14:57:11 +08:00
|
|
|
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
|
|
|
|
|
}
|
2026-07-22 15:20:33 +08:00
|
|
|
if len(apps) == 0 {
|
|
|
|
|
errors = append(errors, fmt.Sprintf("namespace %d has no apps for deployment query", binding.WayneNamespaceID))
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
appID := apps[0].ID
|
2026-07-22 14:57:11 +08:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-22 15:20:33 +08:00
|
|
|
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 == "" {
|
2026-07-22 14:57:11 +08:00
|
|
|
continue
|
|
|
|
|
}
|
2026-07-22 15:20:33 +08:00
|
|
|
key := strings.Join([]string{workload.Cluster, workload.Namespace, workload.Workload, workload.Name}, "\x00")
|
|
|
|
|
if _, ok := seenWorkloads[key]; ok {
|
|
|
|
|
continue
|
2026-07-22 14:57:11 +08:00
|
|
|
}
|
2026-07-22 15:20:33 +08:00
|
|
|
seenWorkloads[key] = struct{}{}
|
|
|
|
|
workloads = append(workloads, workload)
|
|
|
|
|
summary.Pods += workload.Pods
|
|
|
|
|
summary.ReadyPods += workload.ReadyPods
|
2026-07-22 14:57:11 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-23 18:36:24 +08:00
|
|
|
func (s *WayneRoleBindingService) ListDeploymentHistories(ctx context.Context, namespaces []model.BusinessLineWayneNamespace, limit int) ([]WayneDeploymentHistory, error) {
|
|
|
|
|
if limit <= 0 {
|
|
|
|
|
limit = 100
|
|
|
|
|
}
|
|
|
|
|
histories := make([]WayneDeploymentHistory, 0)
|
|
|
|
|
seenDeployments := map[int64]struct{}{}
|
|
|
|
|
for _, binding := range namespaces {
|
|
|
|
|
apps, err := s.listWayneNamespaceApps(ctx, binding.WayneNamespaceID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
for _, app := range apps {
|
|
|
|
|
deployments, err := s.listWayneAppDeployments(ctx, app.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
for _, deployment := range deployments {
|
|
|
|
|
if deployment.ID == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if _, ok := seenDeployments[deployment.ID]; ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
seenDeployments[deployment.ID] = struct{}{}
|
|
|
|
|
items, err := s.listWaynePublishHistories(ctx, deployment.ID, 20)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
for _, item := range items {
|
|
|
|
|
item.BusinessLineID = binding.BusinessLineID
|
|
|
|
|
item.NamespaceID = binding.WayneNamespaceID
|
|
|
|
|
if item.ResourceName == "" {
|
|
|
|
|
item.ResourceName = deployment.Name
|
|
|
|
|
}
|
|
|
|
|
histories = append(histories, item)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
sortWayneDeploymentHistories(histories)
|
|
|
|
|
if len(histories) > limit {
|
|
|
|
|
histories = histories[:limit]
|
|
|
|
|
}
|
|
|
|
|
return histories, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *WayneRoleBindingService) GetDeploymentHistory(ctx context.Context, namespaces []model.BusinessLineWayneNamespace, resourceID, historyID int64) (*WayneDeploymentHistory, error) {
|
|
|
|
|
histories, err := s.ListDeploymentHistories(ctx, namespaces, 500)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
for _, history := range histories {
|
|
|
|
|
if history.ResourceID == resourceID && history.ID == historyID {
|
|
|
|
|
return &history, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil, fmt.Errorf("wayne deployment history %d was not found", historyID)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-22 14:57:11 +08:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-23 18:36:24 +08:00
|
|
|
func (s *WayneRoleBindingService) listWayneAppDeployments(ctx context.Context, appID uint64) ([]wayneDeployment, error) {
|
|
|
|
|
values := url.Values{}
|
|
|
|
|
values.Set("pageNo", "1")
|
|
|
|
|
values.Set("pageSize", "500")
|
|
|
|
|
values.Set("deleted", "false")
|
|
|
|
|
result, err := s.callRaw(ctx, http.MethodGet, fmt.Sprintf("/api/v1/apps/%d/deployments?%s", appID, values.Encode()), nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return parseWayneDeployments(result.Body)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *WayneRoleBindingService) listWaynePublishHistories(ctx context.Context, resourceID int64, pageSize int) ([]WayneDeploymentHistory, error) {
|
|
|
|
|
values := url.Values{}
|
|
|
|
|
values.Set("pageNo", "1")
|
|
|
|
|
values.Set("pageSize", strconv.Itoa(pageSize))
|
|
|
|
|
values.Set("type", "0")
|
|
|
|
|
values.Set("resourceId", strconv.FormatInt(resourceID, 10))
|
|
|
|
|
values.Set("sortby", "-createTime")
|
|
|
|
|
result, err := s.callRaw(ctx, http.MethodGet, "/api/v1/publish/histories?"+values.Encode(), nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return parseWaynePublishHistories(result.Body)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-22 14:57:11 +08:00
|
|
|
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"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-23 18:36:24 +08:00
|
|
|
type wayneDeployment struct {
|
|
|
|
|
ID int64 `json:"id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-22 14:57:11 +08:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-23 18:36:24 +08:00
|
|
|
func parseWayneDeployments(body []byte) ([]wayneDeployment, error) {
|
|
|
|
|
var wrapped struct {
|
|
|
|
|
Data struct {
|
|
|
|
|
List []wayneDeployment `json:"list"`
|
|
|
|
|
} `json:"data"`
|
|
|
|
|
}
|
|
|
|
|
if err := json.Unmarshal(body, &wrapped); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return wrapped.Data.List, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parseWaynePublishHistories(body []byte) ([]WayneDeploymentHistory, error) {
|
|
|
|
|
var wrapped struct {
|
|
|
|
|
Data struct {
|
|
|
|
|
List []WayneDeploymentHistory `json:"list"`
|
|
|
|
|
} `json:"data"`
|
|
|
|
|
}
|
|
|
|
|
if err := json.Unmarshal(body, &wrapped); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
for i := range wrapped.Data.List {
|
|
|
|
|
wrapped.Data.List[i].CreatedAt = parseWayneTime(wrapped.Data.List[i].CreateTime)
|
|
|
|
|
}
|
|
|
|
|
return wrapped.Data.List, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-22 14:57:11 +08:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-23 18:36:24 +08:00
|
|
|
func parseWayneTime(raw string) time.Time {
|
|
|
|
|
raw = strings.TrimSpace(raw)
|
|
|
|
|
if raw == "" {
|
|
|
|
|
return time.Time{}
|
|
|
|
|
}
|
|
|
|
|
layouts := []string{time.RFC3339, "2006-01-02T15:04:05Z07:00", "2006-01-02 15:04:05", "2006-01-02T15:04:05"}
|
|
|
|
|
for _, layout := range layouts {
|
|
|
|
|
if parsed, err := time.Parse(layout, raw); err == nil {
|
|
|
|
|
return parsed
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return time.Time{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func sortWayneDeploymentHistories(items []WayneDeploymentHistory) {
|
|
|
|
|
for i := 1; i < len(items); i++ {
|
|
|
|
|
item := items[i]
|
|
|
|
|
j := i - 1
|
|
|
|
|
for j >= 0 && items[j].CreatedAt.Before(item.CreatedAt) {
|
|
|
|
|
items[j+1] = items[j]
|
|
|
|
|
j--
|
|
|
|
|
}
|
|
|
|
|
items[j+1] = item
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-22 14:57:11 +08:00
|
|
|
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)
|
|
|
|
|
}
|