Files
aiops/backend/controller/k8s_yaml.go
T
attacker 77c234b060 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 ✅
2026-06-25 20:16:30 +08:00

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