- 新增 log_datasource.go / log_proxy.go / user_api_token.go 后端控制器 - 新增 log_datasource.go model - 新增 monitor/log-center.ts API 模块 - 新增 LogCenter 前端组件及页面 - 重构 deployment 相关组件 (HPDA/UpdateImageModal/bundle-yaml 等) - 新增 ResetPasswordDialog / code-snippet-block / copy-button 等 UI 组件 - 重构多个 k8s 相关组件 (TopologyView/ResourceVisualEditor/PodFileBrowser 等) - 更新多个 API 和工具函数 (auth.ts/monitor/index.ts/request.ts 等) - 更新静态资源 (index.html + 新 JS/CSS bundle) - 删除废弃静态资源文件
443 lines
13 KiB
Go
443 lines
13 KiB
Go
package controller
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"ezops/model"
|
|
k8sclient "ezops/pkg/kubernetesclient"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func (h *KubernetesHandler) ListNodes(c *gin.Context) {
|
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
|
|
|
client, _, err := h.getK8sClient(clusterID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
|
return
|
|
}
|
|
nodes, err := client.ListNodes()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
// Attempt to fetch metrics (best-effort, metrics-server may not be installed)
|
|
metrics := map[string]k8sclient.NodeMetricsItem{}
|
|
if m, err := client.GetNodeMetrics(); err == nil {
|
|
for _, item := range m.Items {
|
|
metrics[item.Metadata.Name] = item
|
|
}
|
|
}
|
|
|
|
type NodeSummary struct {
|
|
Name string `json:"name"`
|
|
IP string `json:"ip"`
|
|
Role string `json:"role"`
|
|
Status string `json:"status"`
|
|
OS string `json:"os"`
|
|
Kubelet string `json:"kubelet_version"`
|
|
CPU int `json:"cpu"`
|
|
Memory int64 `json:"memory"` // MiB
|
|
MaxPods int `json:"max_pods"`
|
|
// Metrics percent (for list usage bars)
|
|
CPUUsage float64 `json:"cpu_usage,omitempty"`
|
|
MemoryUsage float64 `json:"memory_usage,omitempty"`
|
|
// Metrics absolute (for detail pages)
|
|
CPUUsageM string `json:"cpu_usage_m,omitempty"` // e.g. "122m"
|
|
MemUsageMi string `json:"mem_usage_mi,omitempty"` // e.g. "2105Mi"
|
|
}
|
|
result := []NodeSummary{}
|
|
for _, n := range nodes.Items {
|
|
role := "worker"
|
|
for k := range n.Metadata.Labels {
|
|
if k == "node-role.kubernetes.io/master" || k == "node-role.kubernetes.io/control-plane" {
|
|
role = "master"
|
|
break
|
|
}
|
|
}
|
|
nodeStatus := "NotReady"
|
|
for _, cond := range n.Status.Conditions {
|
|
if cond.Type == "Ready" && cond.Status == "True" {
|
|
nodeStatus = "Ready"
|
|
break
|
|
}
|
|
}
|
|
ip := ""
|
|
for _, addr := range n.Status.Addresses {
|
|
if addr.Type == "InternalIP" {
|
|
ip = addr.Address
|
|
break
|
|
}
|
|
}
|
|
cpuCapacity := parseIntResource(n.Status.Capacity["cpu"])
|
|
memCapacity := parseMemResource(n.Status.Capacity["memory"])
|
|
s := NodeSummary{
|
|
Name: n.Metadata.Name,
|
|
IP: ip,
|
|
Role: role,
|
|
Status: nodeStatus,
|
|
OS: n.Status.NodeInfo.OSImage,
|
|
Kubelet: n.Status.NodeInfo.KubeletVersion,
|
|
CPU: cpuCapacity,
|
|
Memory: memCapacity,
|
|
MaxPods: parseIntResource(n.Status.Capacity["pods"]),
|
|
}
|
|
if m, ok := metrics[n.Metadata.Name]; ok {
|
|
// K8s metrics API returns CPU in nanocores ("80410173n"), memory in Ki ("2154292Ki")
|
|
s.CPUUsageM = formatCpuFromNano(m.Usage["cpu"])
|
|
s.MemUsageMi = formatMemFromKi(m.Usage["memory"])
|
|
// 计算百分比用于列表页使用率条 // Logan.Li
|
|
if cpuCapacity > 0 {
|
|
cpuMillicores := parseCpuNanoToMilli(m.Usage["cpu"])
|
|
s.CPUUsage = float64(cpuMillicores) / float64(cpuCapacity*1000) * 100
|
|
}
|
|
if memCapacity > 0 {
|
|
memMi := parseMemKiToMi(m.Usage["memory"])
|
|
s.MemoryUsage = float64(memMi) / float64(memCapacity) * 100
|
|
}
|
|
}
|
|
result = append(result, s)
|
|
}
|
|
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: result})
|
|
}
|
|
|
|
// GetNodeDetail 获取节点详情(PID/容器运行时/分配率)// M10
|
|
// @Summary 获取节点详情
|
|
// @Description 获取指定集群中单个节点的详细信息,包含系统信息、资源分配、污点、条件等
|
|
// @Tags K8s集群
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Router /kubernetes/{clusterCode}/nodes/{name} [get]
|
|
func (h *KubernetesHandler) GetNodeDetail(c *gin.Context) {
|
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
|
if clusterID == 0 {
|
|
code := c.Param("clusterCode")
|
|
if code == "" {
|
|
code = c.Param("id")
|
|
}
|
|
clusterID = h.resolveClusterCode(code)
|
|
}
|
|
if clusterID == 0 {
|
|
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "cluster not found"})
|
|
return
|
|
}
|
|
|
|
nodeName := c.Param("name")
|
|
if nodeName == "" {
|
|
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "node name required"})
|
|
return
|
|
}
|
|
|
|
client, _, err := h.getK8sClient(clusterID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
// 1. Get node detail
|
|
node, err := client.GetNode(nodeName)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
// 2. Get node metrics (best-effort)
|
|
var cpuUsageM, memUsageMi string
|
|
if m, err := client.GetNodeMetrics(); err == nil {
|
|
for _, item := range m.Items {
|
|
if item.Metadata.Name == nodeName {
|
|
cpuUsageM = formatCpuFromNano(item.Usage["cpu"])
|
|
memUsageMi = formatMemFromKi(item.Usage["memory"])
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Get pods on this node for allocation calculation
|
|
pods, _ := client.ListPods()
|
|
podCount := 0
|
|
type podBrief struct {
|
|
Namespace string `json:"namespace"`
|
|
Name string `json:"name"`
|
|
Status string `json:"status"`
|
|
IP string `json:"ip"`
|
|
CPURequestM int `json:"cpu_request_m"`
|
|
MemRequestMi int64 `json:"mem_request_mi"`
|
|
RestartCount int `json:"restart_count"`
|
|
}
|
|
var podList []podBrief
|
|
totalCPURequestM := 0
|
|
totalMemRequestMi := int64(0)
|
|
for _, p := range pods.Items {
|
|
if p.Spec.NodeName != nodeName {
|
|
continue
|
|
}
|
|
podCount++
|
|
podCPUReq := 0
|
|
podMemReq := int64(0)
|
|
for _, ctr := range p.Spec.Containers {
|
|
if req := ctr.Resources.Requests; req != nil {
|
|
podCPUReq += parseCpuToMilli(req["cpu"])
|
|
podMemReq += parseMemToMi(req["memory"])
|
|
}
|
|
}
|
|
totalCPURequestM += podCPUReq
|
|
totalMemRequestMi += podMemReq
|
|
|
|
status := "Unknown"
|
|
if p.Status.Phase != "" {
|
|
status = p.Status.Phase
|
|
}
|
|
restartCount := 0
|
|
for _, cs := range p.Status.ContainerStatuses {
|
|
restartCount += cs.RestartCount
|
|
}
|
|
podList = append(podList, podBrief{
|
|
Namespace: p.Metadata.Namespace,
|
|
Name: p.Metadata.Name,
|
|
Status: status,
|
|
IP: p.Status.PodIP,
|
|
CPURequestM: podCPUReq,
|
|
MemRequestMi: podMemReq,
|
|
RestartCount: restartCount,
|
|
})
|
|
}
|
|
|
|
// 4. Determine role
|
|
role := "worker"
|
|
for k := range node.Metadata.Labels {
|
|
if k == "node-role.kubernetes.io/master" || k == "node-role.kubernetes.io/control-plane" {
|
|
role = "master"
|
|
break
|
|
}
|
|
}
|
|
|
|
// 5. Determine status
|
|
nodeStatus := "NotReady"
|
|
for _, cond := range node.Status.Conditions {
|
|
if cond.Type == "Ready" && cond.Status == "True" {
|
|
nodeStatus = "Ready"
|
|
break
|
|
}
|
|
}
|
|
|
|
// 6. Get IP
|
|
internalIP := ""
|
|
externalIP := ""
|
|
hostname := ""
|
|
for _, addr := range node.Status.Addresses {
|
|
switch addr.Type {
|
|
case "InternalIP":
|
|
internalIP = addr.Address
|
|
case "ExternalIP":
|
|
externalIP = addr.Address
|
|
case "Hostname":
|
|
hostname = addr.Address
|
|
}
|
|
}
|
|
|
|
// 7. Build conditions list
|
|
type condInfo struct {
|
|
Type string `json:"type"`
|
|
Status string `json:"status"`
|
|
Reason string `json:"reason,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
conditions := []condInfo{}
|
|
for _, cond := range node.Status.Conditions {
|
|
conditions = append(conditions, condInfo{
|
|
Type: cond.Type,
|
|
Status: cond.Status,
|
|
Reason: cond.Reason,
|
|
Message: cond.Message,
|
|
})
|
|
}
|
|
|
|
// 8. Build taints list
|
|
type taintInfo struct {
|
|
Key string `json:"key"`
|
|
Value string `json:"value"`
|
|
Effect string `json:"effect"`
|
|
}
|
|
taints := []taintInfo{}
|
|
for _, t := range node.Spec.Taints {
|
|
taints = append(taints, taintInfo{
|
|
Key: t.Key,
|
|
Value: t.Value,
|
|
Effect: t.Effect,
|
|
})
|
|
}
|
|
|
|
// 9. Calculate allocation rate
|
|
capCPU := parseIntResource(node.Status.Capacity["cpu"])
|
|
allocCPU := parseIntResource(node.Status.Allocatable["cpu"])
|
|
capMem := parseMemResource(node.Status.Capacity["memory"])
|
|
allocMem := parseMemResource(node.Status.Allocatable["memory"])
|
|
capPods := parseIntResource(node.Status.Capacity["pods"])
|
|
allocPods := parseIntResource(node.Status.Allocatable["pods"])
|
|
|
|
cpuReqCores := float64(totalCPURequestM) / 1000.0
|
|
cpuAllocPct := 0.0
|
|
if allocCPU > 0 {
|
|
cpuAllocPct = (cpuReqCores / float64(allocCPU)) * 100
|
|
}
|
|
memAllocPct := 0.0
|
|
if allocMem > 0 {
|
|
memAllocPct = (float64(totalMemRequestMi) / float64(allocMem)) * 100
|
|
}
|
|
podAllocPct := 0.0
|
|
if allocPods > 0 {
|
|
podAllocPct = (float64(podCount) / float64(allocPods)) * 100
|
|
}
|
|
|
|
// 10. Build labels list
|
|
type labelEntry struct {
|
|
Key string `json:"key"`
|
|
Value string `json:"value"`
|
|
}
|
|
labels := []labelEntry{}
|
|
for k, v := range node.Metadata.Labels {
|
|
labels = append(labels, labelEntry{Key: k, Value: v})
|
|
}
|
|
|
|
detail := map[string]interface{}{
|
|
"name": node.Metadata.Name,
|
|
"role": role,
|
|
"status": nodeStatus,
|
|
"creation_time": node.Metadata.CreationTimestamp,
|
|
"internal_ip": internalIP,
|
|
"external_ip": externalIP,
|
|
"hostname": hostname,
|
|
"pod_cidr": node.Spec.PodCIDR,
|
|
"container_runtime": node.Status.NodeInfo.ContainerRuntimeVersion,
|
|
"kubelet_version": node.Status.NodeInfo.KubeletVersion,
|
|
"os_image": node.Status.NodeInfo.OSImage,
|
|
"kernel_version": node.Status.NodeInfo.KernelVersion,
|
|
"architecture": node.Status.NodeInfo.Architecture,
|
|
"operating_system": node.Status.NodeInfo.OperatingSystem,
|
|
// Capacity & Allocatable
|
|
"capacity": node.Status.Capacity,
|
|
"allocatable": node.Status.Allocatable,
|
|
// Metrics
|
|
"cpu_usage_m": cpuUsageM,
|
|
"mem_usage_mi": memUsageMi,
|
|
// Allocation
|
|
"pod_count": podCount,
|
|
"cpu_request_m": totalCPURequestM,
|
|
"mem_request_mi": totalMemRequestMi,
|
|
"cpu_alloc_pct": cpuAllocPct,
|
|
"mem_alloc_pct": memAllocPct,
|
|
"pod_alloc_pct": podAllocPct,
|
|
"cpu_capacity": capCPU,
|
|
"cpu_allocatable": allocCPU,
|
|
"mem_capacity_mb": capMem,
|
|
"mem_allocatable_mb": allocMem,
|
|
"pod_capacity": capPods,
|
|
"pod_allocatable": allocPods,
|
|
// Lists
|
|
"conditions": conditions,
|
|
"taints": taints,
|
|
"labels": labels,
|
|
"pods": podList,
|
|
}
|
|
|
|
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: detail})
|
|
}
|
|
|
|
// parseCpuToMilli converts CPU resource string to millicores (e.g. "100m"→100, "2"→2000)
|
|
func parseCpuToMilli(val string) int {
|
|
if val == "" {
|
|
return 0
|
|
}
|
|
if strings.HasSuffix(val, "m") {
|
|
var i int
|
|
fmt.Sscanf(strings.TrimSuffix(val, "m"), "%d", &i)
|
|
return i
|
|
}
|
|
var i int
|
|
fmt.Sscanf(val, "%d", &i)
|
|
return i * 1000
|
|
}
|
|
|
|
// parseMemToMi converts memory resource string to Mi (e.g. "256Mi"→256, "1Gi"→1024, "512Ki"→0)
|
|
func parseMemToMi(val string) int64 {
|
|
if val == "" {
|
|
return 0
|
|
}
|
|
var num float64
|
|
var unit string
|
|
fmt.Sscanf(val, "%f%s", &num, &unit)
|
|
switch unit {
|
|
case "Ki":
|
|
return int64(num / 1024)
|
|
case "Mi":
|
|
return int64(num)
|
|
case "Gi":
|
|
return int64(num * 1024)
|
|
case "Ti":
|
|
return int64(num * 1024 * 1024)
|
|
default:
|
|
return int64(num / 1024 / 1024)
|
|
}
|
|
}
|
|
|
|
// ListPods 获取Pod列表
|
|
// @Summary 获取Pod列表
|
|
// @Description 获取指定集群的Pod列表
|
|
// @Tags K8s集群
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param id path int true "集群ID"
|
|
// @Param namespace query string false "命名空间"
|
|
// @Success 200 {object} model.Response{data=[]model.KubernetesPod}
|
|
// @Router /kubernetes-clusters/{id}/pods [get]
|
|
func (h *KubernetesHandler) GetCapacityAnalysis(c *gin.Context) {
|
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
|
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
|
client, _, err := h.getK8sClient(clusterID)
|
|
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
|
|
|
ns := c.Query("namespace")
|
|
deps, err := client.ListDeployments()
|
|
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
|
|
|
type ContainerResource struct {
|
|
Name string `json:"name"`
|
|
CPUReq string `json:"cpu_request"`
|
|
CPULim string `json:"cpu_limit"`
|
|
MemReq string `json:"mem_request"`
|
|
MemLim string `json:"mem_limit"`
|
|
}
|
|
type CapacityRow struct {
|
|
Namespace string `json:"namespace"`
|
|
Name string `json:"name"`
|
|
Replicas int `json:"replicas"`
|
|
Containers []ContainerResource `json:"containers"`
|
|
}
|
|
result := []CapacityRow{}
|
|
for _, d := range deps.Items {
|
|
if ns != "" && d.Metadata.Namespace != ns { continue }
|
|
containers := []ContainerResource{}
|
|
for _, c := range d.Spec.Template.Spec.Containers {
|
|
cpuReq := c.Resources.Requests["cpu"]
|
|
cpuLim := c.Resources.Limits["cpu"]
|
|
memReq := c.Resources.Requests["memory"]
|
|
memLim := c.Resources.Limits["memory"]
|
|
containers = append(containers, ContainerResource{c.Name, cpuReq, cpuLim, memReq, memLim})
|
|
}
|
|
result = append(result, CapacityRow{d.Metadata.Namespace, d.Metadata.Name, d.Spec.Replicas, containers})
|
|
}
|
|
c.JSON(200, model.Response{Code: 200, Data: result})
|
|
}
|
|
|
|
// ============================================================================
|
|
// YAML Apply (kubectl apply -f)
|
|
// ============================================================================
|
|
|
|
// ApplyYAML applies a YAML manifest to create or update a resource
|