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,384 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"ezops/model"
|
||||
k8sclient "ezops/pkg/kubernetesclient"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (h *KubernetesHandler) ListHPAs(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
|
||||
}
|
||||
|
||||
hpas, err := client.ListHPAs()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: hpas.Items})
|
||||
}
|
||||
|
||||
// CreateHPA 创建 HPA
|
||||
func (h *KubernetesHandler) CreateHPA(c *gin.Context) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var req k8sclient.CreateHPARequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Name == "" || req.Namespace == "" || req.TargetName == "" {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "name/namespace/targetName 必填"})
|
||||
return
|
||||
}
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
hpa, err := client.CreateHPA(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建 HPA 失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "HPA 创建成功", Data: hpa})
|
||||
}
|
||||
|
||||
// DeleteHPA 删除 HPA
|
||||
func (h *KubernetesHandler) DeleteHPA(c *gin.Context) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
namespace := c.Param("namespace")
|
||||
name := c.Param("name")
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.DeleteHPA(namespace, name); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除 HPA 失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "HPA 已删除"})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CronJob 管理
|
||||
// ============================================================================
|
||||
|
||||
// ListCronJobs 获取 CronJob 列表
|
||||
func (h *KubernetesHandler) ListCronJobs(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
|
||||
}
|
||||
|
||||
cronjobs, err := client.ListCronJobs()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: cronjobs.Items})
|
||||
}
|
||||
|
||||
// CreateCronJob 创建 CronJob
|
||||
func (h *KubernetesHandler) CreateCronJob(c *gin.Context) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
|
||||
var req k8sclient.CreateCronJobRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
cj, err := client.CreateCronJob(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建 CronJob 失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "CronJob 创建成功", Data: cj})
|
||||
}
|
||||
|
||||
// SuspendCronJob 暂停/恢复 CronJob
|
||||
func (h *KubernetesHandler) SuspendCronJob(c *gin.Context) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
namespace := c.Param("namespace")
|
||||
name := c.Param("name")
|
||||
|
||||
var req struct {
|
||||
Suspend bool `json:"suspend"`
|
||||
}
|
||||
c.ShouldBindJSON(&req)
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.SuspendCronJob(namespace, name, req.Suspend); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "操作失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
action := "恢复"
|
||||
if req.Suspend {
|
||||
action = "暂停"
|
||||
}
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "CronJob 已" + action})
|
||||
}
|
||||
|
||||
// DeleteCronJob 删除 CronJob
|
||||
func (h *KubernetesHandler) DeleteCronJob(c *gin.Context) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
namespace := c.Param("namespace")
|
||||
name := c.Param("name")
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.DeleteCronJob(namespace, name); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除 CronJob 失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "CronJob 已删除"})
|
||||
}
|
||||
|
||||
// TriggerCronJob 手动触发 CronJob
|
||||
func (h *KubernetesHandler) TriggerCronJob(c *gin.Context) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
namespace := c.Param("namespace")
|
||||
name := c.Param("name")
|
||||
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.TriggerCronJob(namespace, name); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "触发失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "手动触发成功"})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Deployment CI 集成 (Webhook 更新镜像)
|
||||
// ============================================================================
|
||||
|
||||
// UpdateDeploymentImage 更新 Deployment 容器镜像 (CI webhook)
|
||||
func (h *KubernetesHandler) ListHPAsNS(c *gin.Context) {
|
||||
h.listHPAsWithNS(c, c.Param("namespace"))
|
||||
}
|
||||
func (h *KubernetesHandler) CreateHPANS(c *gin.Context) {
|
||||
h.createHPAWithNS(c, c.Param("namespace"))
|
||||
}
|
||||
func (h *KubernetesHandler) DeleteHPANS(c *gin.Context) {
|
||||
h.deleteHPAWithNS(c, c.Param("namespace"))
|
||||
}
|
||||
func (h *KubernetesHandler) ListCronJobsNS(c *gin.Context) {
|
||||
h.listCronJobsWithNS(c, c.Param("namespace"))
|
||||
}
|
||||
func (h *KubernetesHandler) CreateCronJobNS(c *gin.Context) {
|
||||
h.createCronJobWithNS(c, c.Param("namespace"))
|
||||
}
|
||||
func (h *KubernetesHandler) SuspendCronJobNS(c *gin.Context) {
|
||||
h.suspendCronJobWithNS(c, c.Param("namespace"))
|
||||
}
|
||||
func (h *KubernetesHandler) TriggerCronJobNS(c *gin.Context) {
|
||||
h.triggerCronJobWithNS(c, c.Param("namespace"))
|
||||
}
|
||||
func (h *KubernetesHandler) DeleteCronJobNS(c *gin.Context) {
|
||||
h.deleteCronJobWithNS(c, c.Param("namespace"))
|
||||
}
|
||||
func (h *KubernetesHandler) listHPAsWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
hpas, err := client.ListHPAs()
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
type HPASummary struct {
|
||||
Namespace string `json:"namespace"`
|
||||
Name string `json:"name"`
|
||||
TargetKind string `json:"target_kind"`
|
||||
TargetName string `json:"target_name"`
|
||||
MinReplicas int `json:"min_replicas"`
|
||||
MaxReplicas int `json:"max_replicas"`
|
||||
CPUPercent int `json:"cpu_percent"`
|
||||
MemPercent int `json:"mem_percent"`
|
||||
CurrentReplicas int `json:"current_replicas"`
|
||||
}
|
||||
result := []HPASummary{}
|
||||
for _, h := range hpas.Items {
|
||||
if h.Metadata.Namespace != ns { continue }
|
||||
cpu := 0; mem := 0
|
||||
for _, m := range h.Spec.Metrics {
|
||||
if m.Resource != nil {
|
||||
if m.Resource.Name == "cpu" { cpu = m.Resource.Target.AverageUtilization }
|
||||
if m.Resource.Name == "memory" { mem = m.Resource.Target.AverageUtilization }
|
||||
}
|
||||
}
|
||||
result = append(result, HPASummary{
|
||||
Namespace: h.Metadata.Namespace,
|
||||
Name: h.Metadata.Name,
|
||||
TargetKind: h.Spec.ScaleTargetRef.Kind,
|
||||
TargetName: h.Spec.ScaleTargetRef.Name,
|
||||
MinReplicas: h.Spec.MinReplicas,
|
||||
MaxReplicas: h.Spec.MaxReplicas,
|
||||
CPUPercent: cpu,
|
||||
MemPercent: mem,
|
||||
CurrentReplicas: h.Status.CurrentReplicas,
|
||||
})
|
||||
}
|
||||
c.JSON(200, model.Response{Code: 200, Data: result})
|
||||
}
|
||||
func (h *KubernetesHandler) createHPAWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
var req k8sclient.CreateHPARequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误"}); return }
|
||||
if req.Namespace == "" { req.Namespace = ns }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
hpa, err := client.CreateHPA(req)
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "创建失败: " + err.Error()}); return }
|
||||
c.JSON(200, model.Response{Code: 200, Data: hpa})
|
||||
}
|
||||
func (h *KubernetesHandler) deleteHPAWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err := client.DeleteHPA(ns, c.Param("name")); err != nil { c.JSON(500, model.Response{Code: 500, Message: "删除失败: " + err.Error()}); return }
|
||||
c.JSON(200, model.Response{Code: 200, Message: "已删除"})
|
||||
}
|
||||
func (h *KubernetesHandler) listCronJobsWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
cjs, err := client.ListCronJobs()
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
filtered := []k8sclient.CronJobItem{}
|
||||
for _, cj := range cjs.Items { if cj.Metadata.Namespace == ns { filtered = append(filtered, cj) } }
|
||||
c.JSON(200, model.Response{Code: 200, Data: filtered})
|
||||
}
|
||||
func (h *KubernetesHandler) createCronJobWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
var req k8sclient.CreateCronJobRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误"}); return }
|
||||
if req.Namespace == "" { req.Namespace = ns }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
cj, err := client.CreateCronJob(req)
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "创建失败: " + err.Error()}); return }
|
||||
c.JSON(200, model.Response{Code: 200, Data: cj})
|
||||
}
|
||||
func (h *KubernetesHandler) suspendCronJobWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
var req struct { Suspend bool `json:"suspend"` }; c.ShouldBindJSON(&req)
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err := client.SuspendCronJob(ns, c.Param("name"), req.Suspend); err != nil { c.JSON(500, model.Response{Code: 500, Message: "操作失败: " + err.Error()}); return }
|
||||
action := "恢复"; if req.Suspend { action = "暂停" }
|
||||
c.JSON(200, model.Response{Code: 200, Message: "CronJob 已" + action})
|
||||
}
|
||||
func (h *KubernetesHandler) triggerCronJobWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err := client.TriggerCronJob(ns, c.Param("name")); err != nil { c.JSON(500, model.Response{Code: 500, Message: "触发失败: " + err.Error()}); return }
|
||||
c.JSON(200, model.Response{Code: 200, Message: "手动触发成功"})
|
||||
}
|
||||
func (h *KubernetesHandler) deleteCronJobWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
client, _, err := h.getK8sClient(clusterID)
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||||
if err := client.DeleteCronJob(ns, c.Param("name")); err != nil { c.JSON(500, model.Response{Code: 500, Message: "删除失败: " + err.Error()}); return }
|
||||
c.JSON(200, model.Response{Code: 200, Message: "已删除"})
|
||||
}
|
||||
func (h *KubernetesHandler) ListHPAsAllNS(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 }
|
||||
hpas, err := client.ListHPAs()
|
||||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||
type HPASummary struct {
|
||||
Namespace string `json:"namespace"`
|
||||
Name string `json:"name"`
|
||||
TargetKind string `json:"target_kind"`
|
||||
TargetName string `json:"target_name"`
|
||||
MinReplicas int `json:"min_replicas"`
|
||||
MaxReplicas int `json:"max_replicas"`
|
||||
CPUPercent int `json:"cpu_percent"`
|
||||
MemPercent int `json:"mem_percent"`
|
||||
CurrentReplicas int `json:"current_replicas"`
|
||||
}
|
||||
result := []HPASummary{}
|
||||
for _, h := range hpas.Items {
|
||||
cpu := 0; mem := 0
|
||||
for _, m := range h.Spec.Metrics {
|
||||
if m.Resource != nil {
|
||||
if m.Resource.Name == "cpu" { cpu = m.Resource.Target.AverageUtilization }
|
||||
if m.Resource.Name == "memory" { mem = m.Resource.Target.AverageUtilization }
|
||||
}
|
||||
}
|
||||
result = append(result, HPASummary{
|
||||
Namespace: h.Metadata.Namespace,
|
||||
Name: h.Metadata.Name,
|
||||
TargetKind: h.Spec.ScaleTargetRef.Kind,
|
||||
TargetName: h.Spec.ScaleTargetRef.Name,
|
||||
MinReplicas: h.Spec.MinReplicas,
|
||||
MaxReplicas: h.Spec.MaxReplicas,
|
||||
CPUPercent: cpu,
|
||||
MemPercent: mem,
|
||||
CurrentReplicas: h.Status.CurrentReplicas,
|
||||
})
|
||||
}
|
||||
c.JSON(200, model.Response{Code: 200, Data: result})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helpers
|
||||
// ============================================================================
|
||||
|
||||
// buildPodMetricsMap fetches pod metrics and returns a map keyed by "namespace/podname" → aggregated CPU/Memory strings.
|
||||
// Best-effort: returns empty map if metrics-server is unavailable. // Logan.Li M2
|
||||
Reference in new issue
Block a user