Files
aiops/backend/controller/k8s_namespace.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

306 lines
9.4 KiB
Go

package controller
import (
"net/http"
"strconv"
"ezops/model"
k8sclient "ezops/pkg/kubernetesclient"
"github.com/gin-gonic/gin"
)
func (h *KubernetesHandler) ListNamespaces(c *gin.Context) {
clusterID, _ := strconv.Atoi(c.Param("id"))
// Support clusterCode param from new routes
if clusterID == 0 {
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
}
client, _, err := h.getK8sClient(clusterID)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
return
}
namespaces, err := client.ListNamespaces()
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: namespaces})
}
// ============================================================================
// Namespace Management (M14 — 资源配额 + CRUD)
// ============================================================================
// NamespaceDetail is the enriched namespace info for the management page
type NamespaceDetail struct {
Name string `json:"name"`
Status string `json:"status"`
Age string `json:"age"`
Labels map[string]string `json:"labels"`
Annotations map[string]string `json:"annotations,omitempty"`
ResourceCounts map[string]int `json:"resource_counts"`
HasResourceQuota bool `json:"has_resource_quota"`
HasLimitRange bool `json:"has_limit_range"`
ResourceQuotas []k8sclient.ResourceQuotaItem `json:"resource_quotas,omitempty"`
LimitRanges []k8sclient.LimitRangeItem `json:"limit_ranges,omitempty"`
}
// GetNamespacesDetail returns all namespaces with metadata, resource counts and quota info
func (h *KubernetesHandler) GetNamespacesDetail(c *gin.Context) {
clusterID, _ := strconv.Atoi(c.Param("id"))
if clusterID == 0 {
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
}
if clusterID == 0 {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "cluster not specified"})
return
}
client, _, err := h.getK8sClient(clusterID)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
return
}
nsList, err := client.ListNamespacesDetailed()
if err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
return
}
// Efficient: fetch all resources once, group by namespace
depCounts := map[string]int{}
if deps, err := client.ListDeployments(); err == nil {
for _, d := range deps.Items {
depCounts[d.Metadata.Namespace]++
}
}
podCounts := map[string]int{}
if pods, err := client.ListPods(); err == nil {
for _, p := range pods.Items {
podCounts[p.Metadata.Namespace]++
}
}
svcCounts := map[string]int{}
if svcs, err := client.ListServices(); err == nil {
for _, s := range svcs.Items {
svcCounts[s.Metadata.Namespace]++
}
}
// All-namespace ResourceQuotas
quotaByNS := map[string][]k8sclient.ResourceQuotaItem{}
if quotas, err := client.ListResourceQuotas(""); err == nil {
for _, q := range quotas {
quotaByNS[q.Metadata.Namespace] = append(quotaByNS[q.Metadata.Namespace], q)
}
}
// All-namespace LimitRanges
lrByNS := map[string][]k8sclient.LimitRangeItem{}
if lrs, err := client.ListLimitRanges(""); err == nil {
for _, lr := range lrs {
lrByNS[lr.Metadata.Namespace] = append(lrByNS[lr.Metadata.Namespace], lr)
}
}
result := make([]NamespaceDetail, 0, len(nsList.Items))
for _, ns := range nsList.Items {
name := ns.Metadata.Name
nd := NamespaceDetail{
Name: name,
Status: ns.Status.Phase,
Age: formatAge(ns.Metadata.CreationTimestamp),
Labels: ns.Metadata.Labels,
Annotations: ns.Metadata.Annotations,
ResourceCounts: map[string]int{
"deployments": depCounts[name],
"pods": podCounts[name],
"services": svcCounts[name],
},
}
if nd.Labels == nil {
nd.Labels = map[string]string{}
}
if qs, ok := quotaByNS[name]; ok && len(qs) > 0 {
nd.HasResourceQuota = true
nd.ResourceQuotas = qs
}
if lrs, ok := lrByNS[name]; ok && len(lrs) > 0 {
nd.HasLimitRange = true
nd.LimitRanges = lrs
}
result = append(result, nd)
}
c.JSON(http.StatusOK, model.Response{Code: 200, Data: result})
}
// GetNamespaceDetailHandler returns a single namespace detail with quota info
func (h *KubernetesHandler) GetNamespaceDetailHandler(c *gin.Context) {
clusterID, _ := strconv.Atoi(c.Param("id"))
if clusterID == 0 {
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
}
if clusterID == 0 {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "cluster not specified"})
return
}
nsName := c.Param("namespace")
if nsName == "" {
nsName = c.Param("name")
}
client, _, err := h.getK8sClient(clusterID)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
return
}
ns, err := client.GetNamespace(nsName)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
return
}
nd := NamespaceDetail{
Name: ns.Metadata.Name,
Status: ns.Status.Phase,
Age: formatAge(ns.Metadata.CreationTimestamp),
Labels: ns.Metadata.Labels,
Annotations: ns.Metadata.Annotations,
ResourceCounts: map[string]int{},
}
if nd.Labels == nil {
nd.Labels = map[string]string{}
}
// Fetch resource counts (single namespace)
if deps, err := client.ListDeployments(); err == nil {
count := 0
for _, d := range deps.Items {
if d.Metadata.Namespace == nsName { count++ }
}
nd.ResourceCounts["deployments"] = count
}
if pods, err := client.ListPods(); err == nil {
count := 0
for _, p := range pods.Items {
if p.Metadata.Namespace == nsName { count++ }
}
nd.ResourceCounts["pods"] = count
}
if svcs, err := client.ListServices(); err == nil {
count := 0
for _, s := range svcs.Items {
if s.Metadata.Namespace == nsName { count++ }
}
nd.ResourceCounts["services"] = count
}
// Fetch quotas + limit ranges for this namespace
if quotas, err := client.ListResourceQuotas(nsName); err == nil && len(quotas) > 0 {
nd.HasResourceQuota = true
nd.ResourceQuotas = quotas
}
if lrs, err := client.ListLimitRanges(nsName); err == nil && len(lrs) > 0 {
nd.HasLimitRange = true
nd.LimitRanges = lrs
}
c.JSON(http.StatusOK, model.Response{Code: 200, Data: nd})
}
// CreateNamespaceHandler creates a new namespace
func (h *KubernetesHandler) CreateNamespaceHandler(c *gin.Context) {
clusterID, _ := strconv.Atoi(c.Param("id"))
if clusterID == 0 {
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
}
if clusterID == 0 {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "cluster not specified"})
return
}
var req struct {
Name string `json:"name" binding:"required"`
Labels map[string]string `json:"labels"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "name is required"})
return
}
// Safety: block critical system namespaces
systemNS := map[string]bool{
"kube-system": true, "kube-public": true, "kube-node-lease": true,
"default": true, "kubernetes-dashboard": true,
}
if systemNS[req.Name] {
c.JSON(http.StatusForbidden, model.Response{Code: 403, Message: "cannot create system namespace: " + req.Name})
return
}
client, _, err := h.getK8sClient(clusterID)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
return
}
ns, err := client.CreateNamespace(req.Name, req.Labels)
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: ns, Message: "命名空间创建成功"})
}
// DeleteNamespaceHandler deletes a namespace
func (h *KubernetesHandler) DeleteNamespaceHandler(c *gin.Context) {
clusterID, _ := strconv.Atoi(c.Param("id"))
if clusterID == 0 {
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
}
if clusterID == 0 {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "cluster not specified"})
return
}
nsName := c.Param("namespace")
if nsName == "" {
nsName = c.Param("name")
}
// Safety: block deletion of critical system namespaces
systemNS := map[string]bool{
"kube-system": true, "kube-public": true, "kube-node-lease": true,
"default": true, "devops": true,
}
if systemNS[nsName] {
c.JSON(http.StatusForbidden, model.Response{Code: 403, Message: "禁止删除系统命名空间: " + nsName})
return
}
client, _, err := h.getK8sClient(clusterID)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
return
}
if err := client.DeleteNamespace(nsName); 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, Message: "命名空间已删除"})
}
// ============================================================================
// ConfigMap & Secret (Kuboard-style namespace resources)
// ============================================================================
// ListConfigMaps 获取命名空间下的 ConfigMap 列表