feat(k8s): M14 Namespace 管理看板(资源配额 + 命名空间 CRUD)

- 后端: ListNamespacesDetailed/CreateNamespace/DeleteNamespace/ListResourceQuotas/ListLimitRanges
- 后端: GetNamespacesDetail handler (6 API calls aggregate, not N×3)
- 后端: CreateNamespaceHandler + DeleteNamespaceHandler (system NS protection)
- 前端: NamespaceManagementPage.tsx (~280行) — 表格+概览卡片+创建/删除弹框
- 路由: GET/POST/DELETE /:clusterCode/namespaces + GET /namespace/:ns/detail
- 验证: 9/9 通过 (urllib), TS 零错误, Vite 构建成功
This commit is contained in:
attacker committed 2026-06-24 23:46:52 +08:00
1 parent c7b61968bc
commit 8240a88a84
33 files changed
+2029 -71

No files matched your search

+96 -2
View File
@@ -62,6 +62,7 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
report := models.AgentReport{
Hostname: req.Hostname,
CPUUsage: req.CPU.UsagePercent,
CPUIOwait: req.CPU.IOWait,
CPUCores: req.CPU.CoreCount,
Load1: req.CPU.Load1,
Load5: req.CPU.Load5,
@@ -69,6 +70,8 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
MemTotal: req.Memory.TotalGB,
MemUsed: req.Memory.UsedGB,
MemUsage: req.Memory.UsagePercent,
MemAvailable: req.Memory.AvailableGB,
MemSwapUsed: req.Memory.SwapUsedGB,
DiskInfo: string(diskJSON),
PortInfo: string(portJSON),
NetInfo: string(netJSON),
@@ -110,7 +113,7 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
// 直接 SQL 更新 asset_servers 监控字段
// 若 AgentVersion 非空则同步写入,否则保留数据库中已有版本号(防止旧版Agent空字段覆盖已设置的版本)
updateSQL := `UPDATE asset_servers SET
cpu_usage = ?, memory_usage = ?, disk_usage = ?,
cpu_usage = ?, cpu_iowait = ?, memory_usage = ?, disk_usage = ?,
cpu = ?, memory = ?, disk = ?,
status = ?, agent_status = ?` + func() string {
if req.AgentVersion != "" {
@@ -121,7 +124,7 @@ func (h *AgentHandler) ReceiveReport(c *gin.Context) {
hostname = CASE WHEN hostname != ? AND ip = ? THEN ? ELSE hostname END,
updated_at = NOW()`
args := []interface{}{
req.CPU.UsagePercent, req.Memory.UsagePercent, diskAvg,
req.CPU.UsagePercent, req.CPU.IOWait, req.Memory.UsagePercent, diskAvg,
req.CPU.CoreCount, memMB, diskGB,
"running", "online",
}
@@ -528,3 +531,94 @@ func avgDiskUsage(disks []models.DiskMetric) float64 {
}
return math.Round(sum/float64(len(disks))*100) / 100
}
// ── 端口监控策略 ── // Logan.Li
// GetPortPolicyForAgent Agent 拉取本机端口策略(无需鉴权,通过 hostname 查询)
// GET /api/v1/agent/port-policy?hostname=xxx
func (h *AgentHandler) GetPortPolicyForAgent(c *gin.Context) {
hostname := c.Query("hostname")
if hostname == "" {
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: models.AgentPortPolicyDTO{
PortFilterMode: "blacklist",
PortWhitelist: []int{},
}})
return
}
var policy models.AgentPortPolicy
if err := h.db.Where("hostname = ?", hostname).First(&policy).Error; err != nil {
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: models.AgentPortPolicyDTO{
Hostname: hostname,
PortFilterMode: "blacklist",
PortWhitelist: []int{},
}})
return
}
var whitelist []int
json.Unmarshal([]byte(policy.PortWhitelist), &whitelist)
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: models.AgentPortPolicyDTO{
Hostname: policy.Hostname,
PortFilterMode: policy.PortFilterMode,
PortWhitelist: whitelist,
}})
}
// GetPortPolicy 前端获取主机的端口监控策略
// GET /api/v1/agent/port-policies/:hostname
func (h *AgentHandler) GetPortPolicy(c *gin.Context) {
hostname := c.Param("hostname")
var policy models.AgentPortPolicy
var whitelist []int
if err := h.db.Where("hostname = ?", hostname).First(&policy).Error; err != nil {
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: models.AgentPortPolicyDTO{
Hostname: hostname,
PortFilterMode: "blacklist",
PortWhitelist: []int{},
}})
return
}
json.Unmarshal([]byte(policy.PortWhitelist), &whitelist)
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: models.AgentPortPolicyDTO{
Hostname: policy.Hostname,
PortFilterMode: policy.PortFilterMode,
PortWhitelist: whitelist,
}})
}
// SavePortPolicy 更新/创建主机的端口监控策略
// PUT /api/v1/agent/port-policies/:hostname
func (h *AgentHandler) SavePortPolicy(c *gin.Context) {
hostname := c.Param("hostname")
var dto models.AgentPortPolicyDTO
if err := c.ShouldBindJSON(&dto); err != nil {
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求格式错误: " + err.Error()})
return
}
whitelistJSON := "[]"
if len(dto.PortWhitelist) > 0 {
b, _ := json.Marshal(dto.PortWhitelist)
whitelistJSON = string(b)
}
policy := models.AgentPortPolicy{
Hostname: hostname,
PortFilterMode: dto.PortFilterMode,
PortWhitelist: whitelistJSON,
}
if err := h.db.Where(models.AgentPortPolicy{Hostname: hostname}).
Assign(policy).
FirstOrCreate(&policy).Error; err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "保存失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "端口策略已保存"})
}
+1 -1
View File
@@ -287,7 +287,7 @@ func (h *DashboardHandler) GetRecentReleases(c *gin.Context) {
// @Success 200 {object} models.Response{data=[]models.MetricData}
// @Router /dashboard/metrics [get]
func (h *DashboardHandler) GetMetrics(c *gin.Context) {
hours := 24
hours := 6 // 默认6小时,减少扫描行数
if hv := c.Query("hours"); hv != "" {
if parsed, err := strconv.Atoi(hv); err == nil && parsed > 0 && parsed <= 168 {
hours = parsed
+266
View File
@@ -1740,6 +1740,272 @@ func (h *KubernetesHandler) ListNamespaces(c *gin.Context) {
c.JSON(http.StatusOK, models.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, models.Response{Code: 400, Message: "cluster not specified"})
return
}
client, _, err := h.getK8sClient(clusterID)
if err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
return
}
nsList, err := client.ListNamespacesDetailed()
if err != nil {
c.JSON(http.StatusInternalServerError, models.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, models.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, models.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, models.Response{Code: 500, Message: err.Error()})
return
}
ns, err := client.GetNamespace(nsName)
if err != nil {
c.JSON(http.StatusInternalServerError, models.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, models.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, models.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, models.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, models.Response{Code: 403, Message: "cannot create system namespace: " + req.Name})
return
}
client, _, err := h.getK8sClient(clusterID)
if err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
return
}
ns, err := client.CreateNamespace(req.Name, req.Labels)
if err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
return
}
c.JSON(http.StatusOK, models.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, models.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, models.Response{Code: 403, Message: "禁止删除系统命名空间: " + nsName})
return
}
client, _, err := h.getK8sClient(clusterID)
if err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
return
}
if err := client.DeleteNamespace(nsName); err != nil {
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
return
}
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "命名空间已删除"})
}
// ============================================================================
// ConfigMap & Secret (Kuboard-style namespace resources)
// ============================================================================
+16
View File
@@ -389,6 +389,11 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
// Namespace list
kubernetesRouter.GET("/:clusterCode/namespaces", k8sHandler.ListNamespaces)
// 🆕 M14: Namespace 管理看板 (detail + CRUD)
kubernetesRouter.GET("/:clusterCode/namespaces/detail", k8sHandler.GetNamespacesDetail)
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/detail", k8sHandler.GetNamespaceDetailHandler)
kubernetesRouter.POST("/:clusterCode/namespaces", k8sHandler.CreateNamespaceHandler)
kubernetesRouter.DELETE("/:clusterCode/namespaces/:namespace", k8sHandler.DeleteNamespaceHandler)
// All-namespace listing (no ns filter)
kubernetesRouter.GET("/:clusterCode/deployments", k8sHandler.ListDeploymentsAllNS)
kubernetesRouter.GET("/:clusterCode/pods", k8sHandler.ListPodsAllNS)
@@ -411,6 +416,11 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/pvcs", k8sHandler.ListPVCsNS)
kubernetesRouter.DELETE("/:clusterCode/namespace/:namespace/pvcs/:name", k8sHandler.DeletePVCNS)
// RBAC — ServiceAccount / Role / RoleBinding
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/serviceaccounts", k8sHandler.ListServiceAccountsNS)
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/roles", k8sHandler.ListRolesNS)
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/rolebindings", k8sHandler.ListRoleBindingsNS)
// StatefulSet / DaemonSet
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/statefulsets", k8sHandler.ListStatefulSetsNS)
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/daemonsets", k8sHandler.ListDaemonSetsNS)
@@ -676,6 +686,8 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
agentGroup.POST("/exec", agentInstallHandler.RemoteExec)
// 健康探针:Agent 拉取本机配置(无需 JWT)
agentGroup.GET("/health-probes/config", agentHandler.GetHealthProbesForAgent)
// 端口策略:Agent 拉取本机端口过滤策略(无需 JWT)
agentGroup.GET("/port-policy", agentHandler.GetPortPolicyForAgent)
}
// Agent 数据查询 + 任务管理(需要 JWT)
agentQuery := authorized.Group("/agent")
@@ -710,6 +722,10 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
agentQuery.POST("/health-probes", agentHandler.CreateHealthProbe)
agentQuery.PUT("/health-probes/:id", agentHandler.UpdateHealthProbe)
agentQuery.DELETE("/health-probes/:id", agentHandler.DeleteHealthProbe)
// 端口策略(CRUD)
agentQuery.GET("/port-policies/:hostname", agentHandler.GetPortPolicy)
agentQuery.PUT("/port-policies/:hostname", agentHandler.SavePortPolicy)
}
// Agent 二进制下载(无需鉴权,Agent 升级时使用)