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:
1 parent
c7b61968bc
commit
8240a88a84
33 files changed
+2029
-71
No files matched your search
Binary file not shown.
Binary file not shown.
@@ -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: "端口策略已保存"})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
// ============================================================================
|
||||
|
||||
@@ -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 升级时使用)
|
||||
|
||||
@@ -9,6 +9,7 @@ type AgentReport struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
Hostname string `gorm:"size:255;index;not null" json:"hostname"`
|
||||
CPUUsage float64 `gorm:"type:decimal(6,2)" json:"cpu_usage"`
|
||||
CPUIOwait float64 `gorm:"type:decimal(6,2)" json:"cpu_iowait"`
|
||||
CPUCores int `json:"cpu_cores"`
|
||||
Load1 float64 `gorm:"type:decimal(8,2)" json:"load1"`
|
||||
Load5 float64 `gorm:"type:decimal(8,2)" json:"load5"`
|
||||
@@ -16,6 +17,8 @@ type AgentReport struct {
|
||||
MemTotal float64 `gorm:"type:decimal(10,2)" json:"mem_total"`
|
||||
MemUsed float64 `gorm:"type:decimal(10,2)" json:"mem_used"`
|
||||
MemUsage float64 `gorm:"type:decimal(6,2)" json:"mem_usage"`
|
||||
MemAvailable float64 `gorm:"type:decimal(10,2)" json:"mem_available"`
|
||||
MemSwapUsed float64 `gorm:"type:decimal(10,2)" json:"mem_swap_used"`
|
||||
DiskInfo string `gorm:"type:text" json:"disk_info"`
|
||||
PortInfo string `gorm:"type:text" json:"port_info"`
|
||||
NetInfo string `gorm:"type:text" json:"net_info"`
|
||||
@@ -141,6 +144,7 @@ type SystemStatModel struct {
|
||||
|
||||
type CPUMetric struct {
|
||||
UsagePercent float64 `json:"usage_percent"`
|
||||
IOWait float64 `json:"iowait"`
|
||||
CoreCount int `json:"core_count"`
|
||||
Load1 float64 `json:"load1"`
|
||||
Load5 float64 `json:"load5"`
|
||||
@@ -151,15 +155,18 @@ type MemMetric struct {
|
||||
TotalGB float64 `json:"total_gb"`
|
||||
UsedGB float64 `json:"used_gb"`
|
||||
CachedGB float64 `json:"cached_gb"`
|
||||
AvailableGB float64 `json:"available_gb"`
|
||||
SwapUsedGB float64 `json:"swap_used_gb"`
|
||||
UsagePercent float64 `json:"usage_percent"`
|
||||
}
|
||||
|
||||
type DiskMetric struct {
|
||||
Mount string `json:"mount"`
|
||||
Device string `json:"device"`
|
||||
TotalGB float64 `json:"total_gb"`
|
||||
UsedGB float64 `json:"used_gb"`
|
||||
UsagePercent float64 `json:"usage_percent"`
|
||||
Mount string `json:"mount"`
|
||||
Device string `json:"device"`
|
||||
TotalGB float64 `json:"total_gb"`
|
||||
UsedGB float64 `json:"used_gb"`
|
||||
UsagePercent float64 `json:"usage_percent"`
|
||||
InodeUsagePct float64 `json:"inode_usage_pct"`
|
||||
}
|
||||
|
||||
type PortMetric struct {
|
||||
@@ -177,6 +184,8 @@ type NetworkStatModel struct {
|
||||
NicName string `json:"nic_name"`
|
||||
NetRxBPS float64 `json:"net_rx_bps"`
|
||||
NetTxBPS float64 `json:"net_tx_bps"`
|
||||
NetRxErrors int64 `json:"net_rx_errors"`
|
||||
NetTxErrors int64 `json:"net_tx_errors"`
|
||||
}
|
||||
|
||||
type DiskIOStatModel struct {
|
||||
@@ -184,4 +193,25 @@ type DiskIOStatModel struct {
|
||||
WriteBPS float64 `json:"write_bps"`
|
||||
ReadIOPS float64 `json:"read_iops"`
|
||||
WriteIOPS float64 `json:"write_iops"`
|
||||
Util float64 `json:"util"`
|
||||
}
|
||||
|
||||
// AgentPortPolicy 主机端口监控策略 // Logan.Li
|
||||
type AgentPortPolicy struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
Hostname string `gorm:"size:255;uniqueIndex;not null" json:"hostname"`
|
||||
PortFilterMode string `gorm:"size:20;default:whitelist;not null" json:"port_filter_mode"` // whitelist / blacklist
|
||||
PortWhitelist string `gorm:"type:text" json:"port_whitelist"` // JSON 数组,如 [80,443,3306,6379]
|
||||
}
|
||||
|
||||
func (AgentPortPolicy) TableName() string {
|
||||
return "monitor_agent_port_policy"
|
||||
}
|
||||
|
||||
// AgentPortPolicyDTO 前端交互用的端口策略格式(含已解析的数组)
|
||||
type AgentPortPolicyDTO struct {
|
||||
Hostname string `json:"hostname"`
|
||||
PortFilterMode string `json:"port_filter_mode"`
|
||||
PortWhitelist []int `json:"port_whitelist"`
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ type AgentRule struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
Name string `gorm:"size:255;not null" json:"name"`
|
||||
Hostname string `gorm:"size:255" json:"hostname"` // 空表示全部主机
|
||||
Metric string `gorm:"size:32;not null" json:"metric"` // cpu/mem/disk/offline/port_down/tcp_time_wait/tcp_close_wait/service_process
|
||||
Metric string `gorm:"size:32;not null" json:"metric"` // cpu/mem/disk/offline/port_down/tcp_time_wait/tcp_close_wait/service_process/fd
|
||||
Operator string `gorm:"size:10;default:>" json:"operator"` // > >= < <= ==
|
||||
Threshold float64 `gorm:"type:decimal(10,2)" json:"threshold"`
|
||||
Port int `json:"port"` // port_down 专用
|
||||
|
||||
@@ -1023,6 +1023,134 @@ func (c *Client) ListNamespaces() ([]string, error) {
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// NamespaceItem holds full namespace metadata
|
||||
type NamespaceItem struct {
|
||||
Metadata ObjectMeta `json:"metadata"`
|
||||
Spec NamespaceSpec `json:"spec"`
|
||||
Status NamespaceStatus `json:"status"`
|
||||
}
|
||||
|
||||
// NamespaceSpec holds namespace spec
|
||||
type NamespaceSpec struct {
|
||||
Finalizers []string `json:"finalizers"`
|
||||
}
|
||||
|
||||
// NamespaceStatus holds namespace phase
|
||||
type NamespaceStatus struct {
|
||||
Phase string `json:"phase"`
|
||||
}
|
||||
|
||||
// NamespaceList wraps a list of NamespaceItem
|
||||
type NamespaceList struct {
|
||||
Items []NamespaceItem `json:"items"`
|
||||
}
|
||||
|
||||
// ListNamespacesDetailed returns all namespaces with full metadata
|
||||
func (c *Client) ListNamespacesDetailed() (*NamespaceList, error) {
|
||||
var result NamespaceList
|
||||
if err := c.get("/api/v1/namespaces", &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetNamespace returns a single namespace by name
|
||||
func (c *Client) GetNamespace(name string) (*NamespaceItem, error) {
|
||||
var result NamespaceItem
|
||||
path := fmt.Sprintf("/api/v1/namespaces/%s", name)
|
||||
if err := c.get(path, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// CreateNamespace creates a new namespace
|
||||
func (c *Client) CreateNamespace(name string, labels map[string]string) (*NamespaceItem, error) {
|
||||
body := map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Namespace",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": name,
|
||||
"labels": labels,
|
||||
},
|
||||
}
|
||||
var result NamespaceItem
|
||||
if err := c.post("/api/v1/namespaces", body, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// DeleteNamespace deletes a namespace by name
|
||||
func (c *Client) DeleteNamespace(name string) error {
|
||||
path := fmt.Sprintf("/api/v1/namespaces/%s", name)
|
||||
return c.delete(path)
|
||||
}
|
||||
|
||||
// ResourceQuotaItem represents a K8s ResourceQuota
|
||||
type ResourceQuotaItem struct {
|
||||
Metadata ObjectMeta `json:"metadata"`
|
||||
Spec ResourceQuotaSpec `json:"spec"`
|
||||
Status ResourceQuotaStatus `json:"status"`
|
||||
}
|
||||
|
||||
// ResourceQuotaSpec holds hard limits
|
||||
type ResourceQuotaSpec struct {
|
||||
Hard map[string]string `json:"hard"`
|
||||
Scopes []string `json:"scopes,omitempty"`
|
||||
}
|
||||
|
||||
// ResourceQuotaStatus holds used/hard counts
|
||||
type ResourceQuotaStatus struct {
|
||||
Hard map[string]string `json:"hard"`
|
||||
Used map[string]string `json:"used"`
|
||||
}
|
||||
|
||||
// ListResourceQuotas returns ResourceQuotas in a namespace, or all namespaces if ns is empty
|
||||
func (c *Client) ListResourceQuotas(namespace string) ([]ResourceQuotaItem, error) {
|
||||
var result struct {
|
||||
Items []ResourceQuotaItem `json:"items"`
|
||||
}
|
||||
var path string
|
||||
if namespace == "" {
|
||||
path = "/api/v1/resourcequotas"
|
||||
} else {
|
||||
path = fmt.Sprintf("/api/v1/namespaces/%s/resourcequotas", namespace)
|
||||
}
|
||||
if err := c.get(path, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Items, nil
|
||||
}
|
||||
|
||||
// LimitRangeItem represents a K8s LimitRange
|
||||
type LimitRangeItem struct {
|
||||
Metadata ObjectMeta `json:"metadata"`
|
||||
Spec LimitRangeSpec `json:"spec"`
|
||||
}
|
||||
|
||||
// LimitRangeSpec holds limit items
|
||||
type LimitRangeSpec struct {
|
||||
Limits []map[string]interface{} `json:"limits"`
|
||||
}
|
||||
|
||||
// ListLimitRanges returns LimitRanges in a namespace, or all namespaces if ns is empty
|
||||
func (c *Client) ListLimitRanges(namespace string) ([]LimitRangeItem, error) {
|
||||
var result struct {
|
||||
Items []LimitRangeItem `json:"items"`
|
||||
}
|
||||
var path string
|
||||
if namespace == "" {
|
||||
path = "/api/v1/limitranges"
|
||||
} else {
|
||||
path = fmt.Sprintf("/api/v1/namespaces/%s/limitranges", namespace)
|
||||
}
|
||||
if err := c.get(path, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Items, nil
|
||||
}
|
||||
|
||||
// --- ConfigMap ---
|
||||
|
||||
// ConfigMapList response
|
||||
@@ -1366,6 +1494,110 @@ func (c *Client) ListEvents(namespace string) (*EventList, error) {
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RBAC — ServiceAccount / Role / RoleBinding
|
||||
// ============================================================================
|
||||
|
||||
// --- ServiceAccount ---
|
||||
|
||||
type ServiceAccountList struct {
|
||||
Items []ServiceAccountItem `json:"items"`
|
||||
}
|
||||
|
||||
type ServiceAccountItem struct {
|
||||
Metadata ObjectMeta `json:"metadata"`
|
||||
Spec struct {
|
||||
ImagePullSecrets []struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"imagePullSecrets"`
|
||||
Secrets []struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"secrets"`
|
||||
} `json:"secrets"`
|
||||
}
|
||||
|
||||
type ServiceAccountRaw struct {
|
||||
Metadata ObjectMeta `json:"metadata"`
|
||||
Secrets []struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"secrets"`
|
||||
ImagePullSecrets []struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"imagePullSecrets"`
|
||||
}
|
||||
|
||||
// ListServiceAccounts fetches ServiceAccounts in a namespace
|
||||
func (c *Client) ListServiceAccounts(namespace string) ([]ServiceAccountRaw, error) {
|
||||
var result struct {
|
||||
Items []ServiceAccountRaw `json:"items"`
|
||||
}
|
||||
path := fmt.Sprintf("/api/v1/namespaces/%s/serviceaccounts", namespace)
|
||||
if err := c.get(path, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Items, nil
|
||||
}
|
||||
|
||||
// --- Role ---
|
||||
|
||||
type RoleList struct {
|
||||
Items []RoleItem `json:"items"`
|
||||
}
|
||||
|
||||
type RoleItem struct {
|
||||
Metadata ObjectMeta `json:"metadata"`
|
||||
Rules []PolicyRule `json:"rules"`
|
||||
}
|
||||
|
||||
type PolicyRule struct {
|
||||
Verbs []string `json:"verbs"`
|
||||
Resources []string `json:"resources"`
|
||||
APIGroups []string `json:"apiGroups"`
|
||||
}
|
||||
|
||||
// ListRoles fetches Roles in a namespace
|
||||
func (c *Client) ListRoles(namespace string) (*RoleList, error) {
|
||||
var result RoleList
|
||||
path := fmt.Sprintf("/apis/rbac.authorization.k8s.io/v1/namespaces/%s/roles", namespace)
|
||||
if err := c.get(path, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// --- RoleBinding ---
|
||||
|
||||
type RoleBindingList struct {
|
||||
Items []RoleBindingItem `json:"items"`
|
||||
}
|
||||
|
||||
type RoleBindingItem struct {
|
||||
Metadata ObjectMeta `json:"metadata"`
|
||||
RoleRef RoleRef `json:"roleRef"`
|
||||
Subjects []Subject `json:"subjects"`
|
||||
}
|
||||
|
||||
type RoleRef struct {
|
||||
Kind string `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type Subject struct {
|
||||
Kind string `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
}
|
||||
|
||||
// ListRoleBindings fetches RoleBindings in a namespace
|
||||
func (c *Client) ListRoleBindings(namespace string) (*RoleBindingList, error) {
|
||||
var result RoleBindingList
|
||||
path := fmt.Sprintf("/apis/rbac.authorization.k8s.io/v1/namespaces/%s/rolebindings", namespace)
|
||||
if err := c.get(path, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// YAML Apply (Dynamic resource apply)
|
||||
// ============================================================================
|
||||
|
||||
Reference in new issue
Block a user