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 ✅
214 lines
10 KiB
Go
214 lines
10 KiB
Go
package controller
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"ezops/model"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func (h *KubernetesHandler) GetResourceYAMLNS(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.Param("namespace")
|
|
resource := c.Param("resource")
|
|
// Fallback: specific routes (statefulsets/:name/yaml, daemonsets/:name/yaml) have no :resource param
|
|
if resource == "" {
|
|
pathParts := strings.Split(c.Request.URL.Path, "/")
|
|
for _, p := range pathParts {
|
|
if p == "statefulsets" || p == "daemonsets" || p == "deployments" || p == "services" || p == "configmaps" || p == "secrets" {
|
|
resource = p
|
|
break
|
|
}
|
|
}
|
|
}
|
|
name := c.Param("name")
|
|
// Build API path based on resource type
|
|
var apiPath string
|
|
switch resource {
|
|
case "deployments", "deployment":
|
|
apiPath = fmt.Sprintf("/apis/apps/v1/namespaces/%s/deployments/%s", ns, name)
|
|
case "statefulsets", "statefulset":
|
|
apiPath = fmt.Sprintf("/apis/apps/v1/namespaces/%s/statefulsets/%s", ns, name)
|
|
case "daemonsets", "daemonset":
|
|
apiPath = fmt.Sprintf("/apis/apps/v1/namespaces/%s/daemonsets/%s", ns, name)
|
|
case "replicasets", "replicaset":
|
|
apiPath = fmt.Sprintf("/apis/apps/v1/namespaces/%s/replicasets/%s", ns, name)
|
|
case "pods", "pod":
|
|
apiPath = fmt.Sprintf("/api/v1/namespaces/%s/pods/%s", ns, name)
|
|
case "services", "service":
|
|
apiPath = fmt.Sprintf("/api/v1/namespaces/%s/services/%s", ns, name)
|
|
case "ingresses", "ingress":
|
|
apiPath = fmt.Sprintf("/apis/networking.k8s.io/v1/namespaces/%s/ingresses/%s", ns, name)
|
|
case "configmaps", "configmap":
|
|
apiPath = fmt.Sprintf("/api/v1/namespaces/%s/configmaps/%s", ns, name)
|
|
case "secrets", "secret":
|
|
apiPath = fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s", ns, name)
|
|
case "pvcs", "persistentvolumeclaim":
|
|
apiPath = fmt.Sprintf("/api/v1/namespaces/%s/persistentvolumeclaims/%s", ns, name)
|
|
case "hpas", "hpa", "horizontalpodautoscaler":
|
|
apiPath = fmt.Sprintf("/apis/autoscaling/v2/namespaces/%s/horizontalpodautoscalers/%s", ns, name)
|
|
case "cronjobs", "cronjob":
|
|
apiPath = fmt.Sprintf("/apis/batch/v1/namespaces/%s/cronjobs/%s", ns, name)
|
|
case "serviceaccounts", "serviceaccount":
|
|
apiPath = fmt.Sprintf("/api/v1/namespaces/%s/serviceaccounts/%s", ns, name)
|
|
case "roles", "role":
|
|
apiPath = fmt.Sprintf("/apis/rbac.authorization.k8s.io/v1/namespaces/%s/roles/%s", ns, name)
|
|
case "rolebindings", "rolebinding":
|
|
apiPath = fmt.Sprintf("/apis/rbac.authorization.k8s.io/v1/namespaces/%s/rolebindings/%s", ns, name)
|
|
default:
|
|
c.JSON(400, model.Response{Code: 400, Message: "unsupported resource type: " + resource}); return
|
|
}
|
|
yamlBytes, err := client.GetResourceYAML(apiPath)
|
|
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "获取YAML失败: " + err.Error()}); return }
|
|
c.Header("Content-Type", "application/yaml")
|
|
c.String(200, string(yamlBytes))
|
|
}
|
|
|
|
// UpdateResourceYAMLNS applies YAML to update a K8s resource
|
|
func (h *KubernetesHandler) UpdateResourceYAMLNS(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.Param("namespace")
|
|
resource := c.Param("resource")
|
|
// Fallback: specific routes (statefulsets/:name/yaml, daemonsets/:name/yaml) have no :resource param
|
|
if resource == "" {
|
|
pathParts := strings.Split(c.Request.URL.Path, "/")
|
|
for _, p := range pathParts {
|
|
if p == "statefulsets" || p == "daemonsets" || p == "deployments" || p == "services" || p == "configmaps" || p == "secrets" {
|
|
resource = p
|
|
break
|
|
}
|
|
}
|
|
}
|
|
name := c.Param("name")
|
|
yamlData, err := io.ReadAll(c.Request.Body)
|
|
if err != nil { c.JSON(400, model.Response{Code: 400, Message: "读取body失败"}); return }
|
|
var apiPath string
|
|
switch resource {
|
|
case "deployments", "deployment":
|
|
apiPath = fmt.Sprintf("/apis/apps/v1/namespaces/%s/deployments/%s", ns, name)
|
|
case "statefulsets", "statefulset":
|
|
apiPath = fmt.Sprintf("/apis/apps/v1/namespaces/%s/statefulsets/%s", ns, name)
|
|
case "daemonsets", "daemonset":
|
|
apiPath = fmt.Sprintf("/apis/apps/v1/namespaces/%s/daemonsets/%s", ns, name)
|
|
case "replicasets", "replicaset":
|
|
apiPath = fmt.Sprintf("/apis/apps/v1/namespaces/%s/replicasets/%s", ns, name)
|
|
case "pods", "pod":
|
|
apiPath = fmt.Sprintf("/api/v1/namespaces/%s/pods/%s", ns, name)
|
|
case "services", "service":
|
|
apiPath = fmt.Sprintf("/api/v1/namespaces/%s/services/%s", ns, name)
|
|
case "ingresses", "ingress":
|
|
apiPath = fmt.Sprintf("/apis/networking.k8s.io/v1/namespaces/%s/ingresses/%s", ns, name)
|
|
case "configmaps", "configmap":
|
|
apiPath = fmt.Sprintf("/api/v1/namespaces/%s/configmaps/%s", ns, name)
|
|
case "secrets", "secret":
|
|
apiPath = fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s", ns, name)
|
|
case "pvcs", "persistentvolumeclaim":
|
|
apiPath = fmt.Sprintf("/api/v1/namespaces/%s/persistentvolumeclaims/%s", ns, name)
|
|
case "hpas", "hpa", "horizontalpodautoscaler":
|
|
apiPath = fmt.Sprintf("/apis/autoscaling/v2/namespaces/%s/horizontalpodautoscalers/%s", ns, name)
|
|
case "cronjobs", "cronjob":
|
|
apiPath = fmt.Sprintf("/apis/batch/v1/namespaces/%s/cronjobs/%s", ns, name)
|
|
case "serviceaccounts", "serviceaccount":
|
|
apiPath = fmt.Sprintf("/api/v1/namespaces/%s/serviceaccounts/%s", ns, name)
|
|
case "roles", "role":
|
|
apiPath = fmt.Sprintf("/apis/rbac.authorization.k8s.io/v1/namespaces/%s/roles/%s", ns, name)
|
|
case "rolebindings", "rolebinding":
|
|
apiPath = fmt.Sprintf("/apis/rbac.authorization.k8s.io/v1/namespaces/%s/rolebindings/%s", ns, name)
|
|
default:
|
|
c.JSON(400, model.Response{Code: 400, Message: "unsupported resource type: " + resource}); return
|
|
}
|
|
if err := client.UpdateResourceYAML(apiPath, yamlData); err != nil {
|
|
c.JSON(500, model.Response{Code: 500, Message: "更新失败: " + err.Error()}); return
|
|
}
|
|
c.JSON(200, model.Response{Code: 200, Message: "YAML已应用"})
|
|
}
|
|
|
|
// resourceAPIPath builds the K8s API path for common resource types (shared by Get/Update/Delete)
|
|
func resourceAPIPath(ns, resource, name string) (string, error) {
|
|
switch resource {
|
|
case "deployments", "deployment":
|
|
return fmt.Sprintf("/apis/apps/v1/namespaces/%s/deployments/%s", ns, name), nil
|
|
case "statefulsets", "statefulset":
|
|
return fmt.Sprintf("/apis/apps/v1/namespaces/%s/statefulsets/%s", ns, name), nil
|
|
case "daemonsets", "daemonset":
|
|
return fmt.Sprintf("/apis/apps/v1/namespaces/%s/daemonsets/%s", ns, name), nil
|
|
case "replicasets", "replicaset":
|
|
return fmt.Sprintf("/apis/apps/v1/namespaces/%s/replicasets/%s", ns, name), nil
|
|
case "pods", "pod":
|
|
return fmt.Sprintf("/api/v1/namespaces/%s/pods/%s", ns, name), nil
|
|
case "services", "service":
|
|
return fmt.Sprintf("/api/v1/namespaces/%s/services/%s", ns, name), nil
|
|
case "ingresses", "ingress":
|
|
return fmt.Sprintf("/apis/networking.k8s.io/v1/namespaces/%s/ingresses/%s", ns, name), nil
|
|
case "configmaps", "configmap":
|
|
return fmt.Sprintf("/api/v1/namespaces/%s/configmaps/%s", ns, name), nil
|
|
case "secrets", "secret":
|
|
return fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s", ns, name), nil
|
|
case "pvcs", "persistentvolumeclaim":
|
|
return fmt.Sprintf("/api/v1/namespaces/%s/persistentvolumeclaims/%s", ns, name), nil
|
|
case "hpas", "hpa", "horizontalpodautoscaler":
|
|
return fmt.Sprintf("/apis/autoscaling/v2/namespaces/%s/horizontalpodautoscalers/%s", ns, name), nil
|
|
case "cronjobs", "cronjob":
|
|
return fmt.Sprintf("/apis/batch/v1/namespaces/%s/cronjobs/%s", ns, name), nil
|
|
case "serviceaccounts", "serviceaccount":
|
|
return fmt.Sprintf("/api/v1/namespaces/%s/serviceaccounts/%s", ns, name), nil
|
|
case "roles", "role":
|
|
return fmt.Sprintf("/apis/rbac.authorization.k8s.io/v1/namespaces/%s/roles/%s", ns, name), nil
|
|
case "rolebindings", "rolebinding":
|
|
return fmt.Sprintf("/apis/rbac.authorization.k8s.io/v1/namespaces/%s/rolebindings/%s", ns, name), nil
|
|
default:
|
|
return "", fmt.Errorf("unsupported resource type: %s", resource)
|
|
}
|
|
}
|
|
|
|
// DeleteResourceYAMLNS deletes a K8s resource (Service/Ingress/ConfigMap etc.)
|
|
func (h *KubernetesHandler) DeleteResourceYAMLNS(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.Param("namespace")
|
|
resource := c.Param("resource")
|
|
name := c.Param("name")
|
|
apiPath, err := resourceAPIPath(ns, resource, name)
|
|
if err != nil { c.JSON(400, model.Response{Code: 400, Message: err.Error()}); return }
|
|
if err := client.DeleteResource(apiPath); err != nil {
|
|
c.JSON(500, model.Response{Code: 500, Message: "删除失败: " + err.Error()}); return
|
|
}
|
|
c.JSON(200, model.Response{Code: 200, Message: "资源已删除"})
|
|
}
|
|
|
|
// getK8sClient builds a K8s API client from cluster's stored kubeconfig
|
|
func (h *KubernetesHandler) ApplyYAML(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 }
|
|
|
|
var req struct {
|
|
YAML string `json:"yaml"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误: " + err.Error()}); return }
|
|
if req.YAML == "" { c.JSON(400, model.Response{Code: 400, Message: "YAML 内容不能为空"}); return }
|
|
|
|
result, err := client.ApplyYAML(req.YAML)
|
|
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "Apply 失败: " + err.Error()}); return }
|
|
|
|
c.JSON(200, model.Response{Code: 200, Message: "资源已应用", Data: result})
|
|
}
|
|
|
|
// ============================================================================
|
|
// ReplicaSet 列表 / Deployment 回滚 / 历史版本 // Logan.Li
|
|
// ============================================================================
|
|
|
|
// ListReplicaSetsNS 列出 namespace 下的 ReplicaSet
|