feat(container): show business line workloads from Wayne
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/1024XEngineer/xinfra/server/internal/auth"
|
||||
"github.com/1024XEngineer/xinfra/server/internal/model"
|
||||
"github.com/1024XEngineer/xinfra/server/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ContainerServiceHandler struct {
|
||||
db *gorm.DB
|
||||
wayne *service.WayneRoleBindingService
|
||||
}
|
||||
|
||||
func NewContainerServiceHandler(db *gorm.DB, wayne *service.WayneRoleBindingService) *ContainerServiceHandler {
|
||||
return &ContainerServiceHandler{db: db, wayne: wayne}
|
||||
}
|
||||
|
||||
func (h *ContainerServiceHandler) Summary(c *gin.Context) {
|
||||
data, ok := h.loadData(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, data.Summary)
|
||||
}
|
||||
|
||||
func (h *ContainerServiceHandler) Workloads(c *gin.Context) {
|
||||
data, ok := h.loadData(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": data.Workloads, "summary": data.Summary})
|
||||
}
|
||||
|
||||
func (h *ContainerServiceHandler) loadData(c *gin.Context) (*service.ContainerServiceData, bool) {
|
||||
claims, ok := CurrentClaims(c)
|
||||
if !ok {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
|
||||
return nil, false
|
||||
}
|
||||
businessLineID, ok := parseBusinessLineID(c)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
businessLine, ok := h.getBusinessLine(c, businessLineID)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
if !h.canReadBusinessLine(c, claims, businessLineID) {
|
||||
return nil, false
|
||||
}
|
||||
namespaces, ok := h.listBusinessLineNamespaces(c, businessLineID)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
data, err := h.wayne.ContainerServiceData(c.Request.Context(), service.ContainerBusinessLineQuery{
|
||||
BusinessLineID: businessLineID,
|
||||
BusinessLineName: businessLine.Name,
|
||||
Namespaces: namespaces,
|
||||
})
|
||||
if err != nil {
|
||||
writeWayneRoleBindingError(c, nil, err)
|
||||
return nil, false
|
||||
}
|
||||
return data, true
|
||||
}
|
||||
|
||||
func (h *ContainerServiceHandler) getBusinessLine(c *gin.Context, id uint64) (model.BusinessLine, bool) {
|
||||
var businessLine model.BusinessLine
|
||||
if err := h.db.First(&businessLine, "id = ?", id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "business line not found"})
|
||||
return businessLine, false
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return businessLine, false
|
||||
}
|
||||
return businessLine, true
|
||||
}
|
||||
|
||||
func (h *ContainerServiceHandler) canReadBusinessLine(c *gin.Context, claims *auth.Claims, businessLineID uint64) bool {
|
||||
if claims.IsAdmin {
|
||||
return true
|
||||
}
|
||||
var binding model.BusinessLineUser
|
||||
if err := h.db.Where("business_line_id = ? AND user_id = ?", businessLineID, claims.UserID).First(&binding).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "current user is not bound to current business line"})
|
||||
return false
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *ContainerServiceHandler) listBusinessLineNamespaces(c *gin.Context, businessLineID uint64) ([]model.BusinessLineWayneNamespace, bool) {
|
||||
var rows []model.BusinessLineWayneNamespace
|
||||
if err := h.db.Where("business_line_id = ?", businessLineID).Order("wayne_namespace_id ASC").Find(&rows).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return nil, false
|
||||
}
|
||||
return rows, true
|
||||
}
|
||||
@@ -83,6 +83,7 @@ func registerAuthServerRoutes(r *gin.Engine, deps Dependencies) {
|
||||
samlHandler := handler.NewSAMLHandler(deps.Config, authService)
|
||||
oauthHandler := handler.NewOAuthHandler(deps.Config, deps.DB, auditService)
|
||||
deploymentHandler := handler.NewDeploymentHandler(deps.Config, deps.DB, deploymentService)
|
||||
containerServiceHandler := handler.NewContainerServiceHandler(deps.DB, wayneRoleBindingService)
|
||||
|
||||
r.GET("/healthz", healthHandler.Healthz)
|
||||
r.GET("/readyz", healthHandler.Readyz)
|
||||
@@ -134,6 +135,8 @@ func registerAuthServerRoutes(r *gin.Engine, deps Dependencies) {
|
||||
protected.PUT("/subsystem-auth/wayne/business-lines/:id/namespaces/:namespaceid/users/:username/roles", subsystemAuthHandler.BindWayneNamespaceRoles)
|
||||
protected.DELETE("/subsystem-auth/wayne/business-lines/:id/namespaces/:namespaceid/users/:username/roles", subsystemAuthHandler.UnbindWayneNamespaceRoles)
|
||||
protected.POST("/subsystem-auth/wayne/business-lines/:id/users/:userid/init", subsystemAuthHandler.InitWayneBusinessLineUser)
|
||||
protected.GET("/container-services/business-lines/:id/summary", containerServiceHandler.Summary)
|
||||
protected.GET("/container-services/business-lines/:id/workloads", containerServiceHandler.Workloads)
|
||||
protected.POST("/deployments", deploymentHandler.Create)
|
||||
protected.GET("/deployments/:id", deploymentHandler.Get)
|
||||
protected.GET("/deployments/:id/events", deploymentHandler.Events)
|
||||
|
||||
@@ -0,0 +1,411 @@
|
||||
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{}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
for _, app := range apps {
|
||||
items, err := s.listWayneDeployments(ctx, app.ID, kubeNamespace, cluster)
|
||||
if err != nil {
|
||||
errors = append(errors, fmt.Sprintf("namespace %d cluster %s app %d deployments: %v", binding.WayneNamespaceID, cluster, app.ID, err))
|
||||
continue
|
||||
}
|
||||
for _, item := range items {
|
||||
workload := containerWorkloadFromMap(item, cluster, kubeNamespace, query.BusinessLineName)
|
||||
if workload.Name == "" {
|
||||
continue
|
||||
}
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user