feat(audit): F1+F2 告警静默规则标记完成 + 审计日志CSV导出实现
F1 告警静默规则 [done]: - 后端 alert_silence.go: CRUD + Toggle (onetime/daily/weekly 三模式) - 告警引擎 isSilenced 集成 (alert_engine.go) - 前端 SilenceRuleTab + SilenceDialog - 路由 /alerts/silences 已注册 F2 审计日志导出 [done]: - 重写 ExportAuditLogs: 真正的 CSV 导出 (encoding/csv + UTF-8 BOM) - 支持 module/action/username/startTime/endTime 筛选 - 导出限制 10000 条防溢出,自动记录审计日志 - JSON 格式向后兼容 (format=json) - 前端 exportLogs 传递查询参数 同时包含累积的前期工作: - 目录重构 internal/ -> controller/model/router (v0.55-v0.57) - 后端分层架构 dto/repository/service/utils - 审计模块全功能 (终端审计/操作审计/登录日志) - asciicast v2 录制格式 - SFTP 文件浏览器 - StatStrip 紧凑统计条 - 在线会话自动刷新 - LiveMonitor WebSocket 诊断修复 验证: go build ✅ | npm run build ✅ | Docker 部署 ✅ | 10 回归端点 ✅ | CSV 导出 ✅ | 静默 CRUD ✅
This commit is contained in:
1 parent
e57718998a
commit
77c234b060
314 files changed
+10050
-4065
No files matched your search
@@ -0,0 +1,428 @@
|
||||
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_mb"`
|
||||
MaxPods int `json:"max_pods"`
|
||||
// Metrics (optional, omitted if empty)
|
||||
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
|
||||
}
|
||||
}
|
||||
s := NodeSummary{
|
||||
Name: n.Metadata.Name,
|
||||
IP: ip,
|
||||
Role: role,
|
||||
Status: nodeStatus,
|
||||
OS: n.Status.NodeInfo.OSImage,
|
||||
Kubelet: n.Status.NodeInfo.KubeletVersion,
|
||||
CPU: parseIntResource(n.Status.Capacity["cpu"]),
|
||||
Memory: parseMemResource(n.Status.Capacity["memory"]),
|
||||
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"])
|
||||
}
|
||||
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
|
||||
Reference in new issue
Block a user