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

+13 -2
View File
@@ -18,8 +18,9 @@ import (
// cpuSample 保存某一时刻 CPU 的累计 jiffies,用于两次采样计算差值
type cpuSample struct {
total uint64 // 自系统启动以来 CPU 所有状态的累计 jiffies
idle uint64 // idle + iowait 的累计 jiffies
total uint64 // 自系统启动以来 CPU 所有状态的累计 jiffies
idle uint64 // idle + iowait 的累计 jiffies
iowait uint64 // iowait 单独累计 jiffies(用于计算 iowait 占比)
}
// CPUCollector CPU 指标采集器。
@@ -50,12 +51,18 @@ func (c *CPUCollector) Collect() (model.CPUStat, error) {
if c.prev != nil {
totalDelta := cur.total - c.prev.total
idleDelta := cur.idle - c.prev.idle
iowaitDelta := cur.iowait - c.prev.iowait
if totalDelta > 0 {
// 使用率 = (总增量 - 空闲增量) / 总增量 * 100
stat.UsagePercent = float64(totalDelta-idleDelta) / float64(totalDelta) * 100.0
if stat.UsagePercent < 0 {
stat.UsagePercent = 0
}
// iowait = iowait增量 / 总增量 * 100
stat.IOWait = float64(iowaitDelta) / float64(totalDelta) * 100.0
if stat.IOWait < 0 {
stat.IOWait = 0
}
}
}
c.prev = &cur
@@ -100,6 +107,10 @@ func readCPUSample() (cpuSample, error) {
if i == 3 || i == 4 {
sample.idle += x
}
// iowait(索引4) 单独记录
if i == 4 {
sample.iowait += x
}
}
return sample, nil
}
+15 -5
View File
@@ -76,12 +76,22 @@ func (d *DiskCollector) Collect() ([]model.DiskStat, error) {
}
used := total - free
// inode 使用率
var inodePct float64
if fs.Files > 0 {
inodePct = float64(fs.Files-fs.Ffree) / float64(fs.Files) * 100.0
if inodePct < 0 {
inodePct = 0
}
}
disks = append(disks, model.DiskStat{
Mount: m.mountpoint,
Device: m.device,
TotalGB: bytesToGB(total),
UsedGB: bytesToGB(used),
UsagePercent: float64(used) / float64(total) * 100.0,
Mount: m.mountpoint,
Device: m.device,
TotalGB: bytesToGB(total),
UsedGB: bytesToGB(used),
UsagePercent: float64(used) / float64(total) * 100.0,
InodeUsagePct: inodePct,
})
}
return disks, nil
+21 -6
View File
@@ -1,5 +1,5 @@
// Package collector — 磁盘 IO 指标采集器
// 通过读取 /proc/diskstats 获取磁盘读写 BPS 和 IOPS。
// 通过读取 /proc/diskstats 获取磁盘读写 BPS、IOPS 和 util(磁盘繁忙程度)。
//
// 署名 Hermes
package collector
@@ -23,6 +23,7 @@ type diskIOSample struct {
writeSects uint64
readOps uint64
writeOps uint64
ioTicks uint64 // io_ticks (field 12) — 磁盘忙碌的毫秒累计
}
// DiskIOCollector 磁盘 IO 采集器
@@ -40,7 +41,7 @@ func NewDiskIOCollector() *DiskIOCollector {
func (c *DiskIOCollector) Collect() (model.DiskIOStat, error) {
stat := model.DiskIOStat{}
rSects, wSects, rOps, wOps, err := readDiskStats()
rSects, wSects, rOps, wOps, ioTicks, err := readDiskStats()
if err != nil {
return stat, fmt.Errorf("读取磁盘 IO 失败: %w", err)
}
@@ -55,6 +56,7 @@ func (c *DiskIOCollector) Collect() (model.DiskIOStat, error) {
writeSects: wSects,
readOps: rOps,
writeOps: wOps,
ioTicks: ioTicks,
}
if c.prev != nil && now.After(c.prev.time) {
@@ -65,6 +67,15 @@ func (c *DiskIOCollector) Collect() (model.DiskIOStat, error) {
stat.WriteBPS = float64((cur.writeSects-c.prev.writeSects)*sectorSize) / elapsed
stat.ReadIOPS = float64(cur.readOps-c.prev.readOps) / elapsed
stat.WriteIOPS = float64(cur.writeOps-c.prev.writeOps) / elapsed
// util% = io_ticks_delta_ms / (elapsed_ms) * 100
ioDelta := cur.ioTicks - c.prev.ioTicks
stat.Util = float64(ioDelta) / (elapsed * 1000.0) * 100.0
if stat.Util > 100 {
stat.Util = 100
}
if stat.Util < 0 {
stat.Util = 0
}
}
}
c.prev = cur
@@ -73,11 +84,11 @@ func (c *DiskIOCollector) Collect() (model.DiskIOStat, error) {
}
// readDiskStats 读取 /proc/diskstats,汇总所有物理磁盘的读写数据
// fields[2]=设备名, fields[3]=读完成次数, fields[5]=读扇区数, fields[7]=写完成次数, fields[9]=写扇区数
func readDiskStats() (rSects, wSects, rOps, wOps uint64, err error) {
// fields[2]=设备名, fields[3]=读完成次数, fields[5]=读扇区数, fields[7]=写完成次数, fields[9]=写扇区数, fields[12]=io_ticks
func readDiskStats() (rSects, wSects, rOps, wOps, ioTicks uint64, err error) {
f, err := os.Open("/proc/diskstats")
if err != nil {
return 0, 0, 0, 0, err
return 0, 0, 0, 0, 0, err
}
defer f.Close()
@@ -103,6 +114,7 @@ func readDiskStats() (rSects, wSects, rOps, wOps uint64, err error) {
// field 5 = 读扇区数 (rd_sectors)
// field 7 = 写完成次数 (wr_ios)
// field 9 = 写扇区数 (wr_sectors)
// field 12 = io_ticks (milliseconds spent doing I/O)
if v, e := strconv.ParseUint(fields[3], 10, 64); e == nil {
rOps += v
}
@@ -115,7 +127,10 @@ func readDiskStats() (rSects, wSects, rOps, wOps uint64, err error) {
if v, e := strconv.ParseUint(fields[9], 10, 64); e == nil {
wSects += v
}
if v, e := strconv.ParseUint(fields[12], 10, 64); e == nil {
ioTicks += v
}
}
return rSects, wSects, rOps, wOps, scanner.Err()
return rSects, wSects, rOps, wOps, ioTicks, scanner.Err()
}
+13 -3
View File
@@ -37,6 +37,8 @@ func (m *MemCollector) Collect() (model.MemoryStat, error) {
cachedKB := values["Cached"]
availableKB := values["MemAvailable"]
freeKB := values["MemFree"]
swapTotalKB := values["SwapTotal"]
swapFreeKB := values["SwapFree"]
// 若内核未提供 MemAvailable(较老版本),则按传统方式估算可用内存
if availableKB == 0 {
@@ -48,10 +50,18 @@ func (m *MemCollector) Collect() (model.MemoryStat, error) {
usedKB = totalKB - availableKB
}
// Swap used
swapUsedKB := uint64(0)
if swapTotalKB > swapFreeKB {
swapUsedKB = swapTotalKB - swapFreeKB
}
stat := model.MemoryStat{
TotalGB: float64(totalKB) * kbToGB,
UsedGB: float64(usedKB) * kbToGB,
CachedGB: float64(buffersKB+cachedKB) * kbToGB,
TotalGB: float64(totalKB) * kbToGB,
UsedGB: float64(usedKB) * kbToGB,
CachedGB: float64(buffersKB+cachedKB) * kbToGB,
AvailableGB: float64(availableKB) * kbToGB,
SwapUsedGB: float64(swapUsedKB) * kbToGB,
}
if totalKB > 0 {
stat.UsagePercent = float64(usedKB) / float64(totalKB) * 100.0
+25 -14
View File
@@ -18,9 +18,11 @@ import (
// netSample 保存上一次的网络采样数据,用于计算速率
type netSample struct {
time time.Time
rxBytes uint64
txBytes uint64
time time.Time
rxBytes uint64
txBytes uint64
rxErrors uint64
txErrors uint64
}
// NetworkCollector 网络指标采集器
@@ -34,7 +36,7 @@ func NewNetworkCollector() *NetworkCollector {
return &NetworkCollector{}
}
// Collect 采集网络指标(并发连接数、TIME_WAIT、网卡 rx/tx B/s)
// Collect 采集网络指标(并发连接数、TIME_WAIT、网卡 rx/tx B/s、错误包)
func (c *NetworkCollector) Collect() (model.NetworkStat, error) {
stat := model.NetworkStat{}
@@ -46,8 +48,8 @@ func (c *NetworkCollector) Collect() (model.NetworkStat, error) {
}
stat.TCPCloseWait = countCloseWait()
// 2. 采集网卡流量(取所有非 lo 网卡合计)
curRx, curTx, nicName, err := readNetDevTotal()
// 2. 采集网卡流量 + 错误包(取所有非 lo 网卡合计)
curRx, curTx, curRxErr, curTxErr, nicName, err := readNetDevTotal()
if err != nil {
return stat, fmt.Errorf("读取网卡流量失败: %w", err)
}
@@ -57,7 +59,7 @@ func (c *NetworkCollector) Collect() (model.NetworkStat, error) {
defer c.mu.Unlock()
now := time.Now()
cur := &netSample{time: now, rxBytes: curRx, txBytes: curTx}
cur := &netSample{time: now, rxBytes: curRx, txBytes: curTx, rxErrors: curRxErr, txErrors: curTxErr}
if c.prev != nil && now.After(c.prev.time) {
elapsed := now.Sub(c.prev.time).Seconds()
@@ -66,6 +68,11 @@ func (c *NetworkCollector) Collect() (model.NetworkStat, error) {
stat.NetTxBPS = float64(cur.txBytes-c.prev.txBytes) / elapsed
}
}
// 错误包:返回当前累计值(供前端判断是否激增)
stat.NetRxErrors = int64(cur.rxErrors)
stat.NetTxErrors = int64(cur.txErrors)
c.prev = cur
return stat, nil
@@ -120,11 +127,11 @@ func readSockStat() (tcpSockStat, error) {
return result, scanner.Err()
}
// readNetDevTotal 读取 /proc/net/dev,取第一个非 lo 且活跃的物理网卡总流量
func readNetDevTotal() (rxBytes, txBytes uint64, nicName string, err error) {
// readNetDevTotal 读取 /proc/net/dev,取第一个非 lo 且活跃的物理网卡总流量 + 错误包
func readNetDevTotal() (rxBytes, txBytes, rxErrors, txErrors uint64, nicName string, err error) {
f, err := os.Open("/proc/net/dev")
if err != nil {
return 0, 0, "", err
return 0, 0, 0, 0, "", err
}
defer f.Close()
@@ -149,26 +156,30 @@ func readNetDevTotal() (rxBytes, txBytes uint64, nicName string, err error) {
}
fields := strings.Fields(parts[1])
if len(fields) < 10 {
if len(fields) < 11 {
continue
}
// fields[0]=rx_bytes, fields[8]=tx_bytes
// fields[0]=rx_bytes, fields[2]=rx_errors, fields[8]=tx_bytes, fields[10]=tx_errors
rb, _ := strconv.ParseUint(fields[0], 10, 64)
tb, _ := strconv.ParseUint(fields[8], 10, 64)
re, _ := strconv.ParseUint(fields[2], 10, 64)
te, _ := strconv.ParseUint(fields[10], 10, 64)
if rb > 0 || tb > 0 {
nicName = name
rxBytes += rb
txBytes += tb
rxErrors += re
txErrors += te
}
}
if nicName == "" {
return 0, 0, "eth0", nil
return 0, 0, 0, 0, "eth0", nil
}
return rxBytes, txBytes, nicName, scanner.Err()
return rxBytes, txBytes, rxErrors, txErrors, nicName, scanner.Err()
}
// countCloseWait 从 /proc/net/tcp 统计 CLOSE_WAIT 连接数
+71 -2
View File
@@ -2,6 +2,8 @@
// 本文件实现监听 TCP 端口的自动发现:
// - 扫描 /proc/net/tcp 与 /proc/net/tcp6 中状态为 LISTEN(0A) 的套接字;
// - 通过遍历 /proc/*/fd 反查套接字 inode 归属的进程。
// - 内置系统进程排除列表(sshd、rpcbind、master 等永远不上报)。
// - 支持远程端口策略:whitelist(全量上报,默认)/ blacklist(只上报白名单端口)。
//
// 署名 Logan.Li
package collector
@@ -15,14 +17,47 @@ import (
"ezops/agent/model"
)
// 系统级进程 — 永远不采集其监听端口
var systemProcesses = map[string]bool{
"sshd": true,
"rpcbind": true,
"master": true, // Postfix
"systemd": true,
"systemd-resolve": true,
"systemd-journal": true,
"dbus-daemon": true,
"rsyslogd": true,
"irqbalance": true,
"polkitd": true,
"accounts-daemon": true,
"NetworkManager": true,
"udisksd": true,
"upowerd": true,
"rtkit-daemon": true,
"switcheroo-contr": true,
"power-profiles-": true, // power-profiles-daemon
"wpa_supplicant": true,
"avahi-daemon": true,
"cron": true,
"atd": true,
"agetty": true,
}
// PortCollector 监听端口自动发现采集器
type PortCollector struct{}
type PortCollector struct {
Policy *model.PortPolicy // 可选:后端下发的端口过滤策略
}
// NewPortCollector 创建端口采集器实例
func NewPortCollector() *PortCollector {
return &PortCollector{}
}
// SetPolicy 设置端口过滤策略(daemon 模式下从后端轮询获得)
func (p *PortCollector) SetPolicy(policy *model.PortPolicy) {
p.Policy = policy
}
// listenSocket 描述一个处于 LISTEN 状态的套接字
type listenSocket struct {
port int
@@ -47,7 +82,7 @@ func (p *PortCollector) Collect() ([]model.PortStat, error) {
// 2. 构建 inode -> 进程信息 映射(仅 Linux 上可用)
inodeMap := p.buildInodeMap()
// 3. 组装结果并按 (端口, 协议, PID) 去重
// 3. 组装结果并按 (端口, 协议, PID) 去重,应用过滤策略
seen := make(map[string]bool)
ports := make([]model.PortStat, 0, len(sockets))
for _, s := range sockets {
@@ -55,6 +90,22 @@ func (p *PortCollector) Collect() ([]model.PortStat, error) {
if info, ok := inodeMap[s.inode]; ok {
pid, name = info.pid, info.name
}
// ── 过滤逻辑 ──
// 1) 永远排除系统级进程端口
if isSystemProcess(name) {
continue
}
// 2) 黑名单模式:只上报在白名单中的端口
if p.Policy != nil && p.Policy.IsBlacklistMode() {
if !p.Policy.InWhitelist(s.port) {
continue
}
}
// 白名单模式(默认):全量上报已排除系统进程的端口
// ── 过滤逻辑结束 ──
key := strconv.Itoa(s.port) + "|" + s.protocol + "|" + strconv.Itoa(pid)
if seen[key] {
continue
@@ -200,3 +251,21 @@ func readProcessName(pid string) string {
}
return ""
}
// isSystemProcess 判断进程名是否为系统进程(永远不上报的端口)
func isSystemProcess(name string) bool {
if name == "" {
return false
}
// 精确匹配
if systemProcesses[name] {
return true
}
// 前缀匹配(如 power-profiles-daemon)
for prefix := range systemProcesses {
if strings.HasPrefix(name, prefix) {
return true
}
}
return false
}