feat(k8s): A1 K8s 双路由体系统一 — code 路由为主, ID 路由标记 deprecated
后端: - 新增 4 条 all-NS code 路由: services/ingresses/cronjobs/nodes - ListNodes 改用 getK8sClientByCode (兼容 ID+code) - ID 域资源路由标记 // Deprecated 前端: - clusterApi/nodeApi/workloadApi 资源方法迁移到 code 路由 - KubernetesClusterDetail/TopologyView/KubernetesNodes 组件从 cluster.id 迁移到 cluster.code - 修复潜在 bug: ClusterOverviewPage 曾将 code 传给 ID-based getNodes 验证: 16/16 API 测试通过 (7 新 code 路由 + 3 旧 ID 兼容 + 6 回归) go build=✅ tsc -b=✅ npm build=✅
This commit is contained in:
1 parent
bb2b91ebe0
commit
00a98e210d
13 files changed
+241
-39
No files matched your search
@@ -125,7 +125,7 @@
|
|||||||
| ~~`kubernetes.go` 4204 行/134 函数~~ → 已拆分为 12 个域文件(最大 696 行) | ✅ 已解决 | 后端 | ~~P0~~ done |
|
| ~~`kubernetes.go` 4204 行/134 函数~~ → 已拆分为 12 个域文件(最大 696 行) | ✅ 已解决 | 后端 | ~~P0~~ done |
|
||||||
| ~~`api.ts` 1324 行/352 方法~~ → 已拆分为 12 个域文件 (api/kubernetes.ts, deploy.ts, monitor.ts 等) | ✅ 已解决 | 前端 | ~~P0~~ done |
|
| ~~`api.ts` 1324 行/352 方法~~ → 已拆分为 12 个域文件 (api/kubernetes.ts, deploy.ts, monitor.ts 等) | ✅ 已解决 | 前端 | ~~P0~~ done |
|
||||||
| 前端 **1026 处** `any` 类型 | 类型安全缺失,重构风险高 | 前端全局 | ~~P0~~ **done** (any 1076→40, unused 25→6, console 93→1, tsc --noEmit=0) |
|
| 前端 **1026 处** `any` 类型 | 类型安全缺失,重构风险高 | 前端全局 | ~~P0~~ **done** (any 1076→40, unused 25→6, console 93→1, tsc --noEmit=0) |
|
||||||
| K8s 双路由体系(集群域+Kuboard 域) | 路由冗余 104 条 | 后端 | P1 |
|
| ~~K8s 双路由体系(集群域+Kuboard 域)~~ → code 路由统一为主,ID 路由标记 deprecated 向后兼容 | ✅ 已解决 | 后端 | ~~P1~~ done |
|
||||||
| 25 个 ≥500 行巨型组件 | 拆分困难 | 前端 | P1 |
|
| 25 个 ≥500 行巨型组件 | 拆分困难 | 前端 | P1 |
|
||||||
|
|
||||||
### 3.2 🟡 中优先级
|
### 3.2 🟡 中优先级
|
||||||
@@ -201,7 +201,7 @@
|
|||||||
|------|:----:|------|
|
|------|:----:|------|
|
||||||
| 功能完整性 | **88/100** | 8 大模块齐全,K8s 管理极为完善;扣分项:监控大屏/链路追踪/告警静默未对接 |
|
| 功能完整性 | **88/100** | 8 大模块齐全,K8s 管理极为完善;扣分项:监控大屏/链路追踪/告警静默未对接 |
|
||||||
| 代码质量 | **82/100** | 后端规范好(0 panic, 1 TODO);前端 any 从 1026 降至 40 + tsc --noEmit 零错误,类型安全大幅提升 |
|
| 代码质量 | **82/100** | 后端规范好(0 panic, 1 TODO);前端 any 从 1026 降至 40 + tsc --noEmit 零错误,类型安全大幅提升 |
|
||||||
| 架构设计 | **82/100** | 前后端分离清晰,自研轻量 K8s 客户端;扣分项:kubernetes.go/api.ts 巨型文件 |
|
| 架构设计 | **85/100** | 前后端分离清晰,自研轻量 K8s 客户端;kubernetes.go/api.ts 已拆分,K8s 双路由已统一为 code 路由为主 (A1 done) |
|
||||||
| 部署运维 | **87/100** | 三种部署方式 + run.sh 统一入口;扣分项:后端镜像偏大 |
|
| 部署运维 | **87/100** | 三种部署方式 + run.sh 统一入口;扣分项:后端镜像偏大 |
|
||||||
| 用户体验 | **86/100** | 骨架屏 + 页面过渡 + 5 套主题 + 懒加载;扣分项:无 i18n,无虚拟滚动 |
|
| 用户体验 | **86/100** | 骨架屏 + 页面过渡 + 5 套主题 + 懒加载;扣分项:无 i18n,无虚拟滚动 |
|
||||||
| **综合** | **85/100** | 企业级运维平台,功能覆盖全面,工程治理 P0 全部完成 |
|
| **综合** | **85/100** | 企业级运维平台,功能覆盖全面,工程治理 P0 全部完成 |
|
||||||
+18
-2
@@ -156,12 +156,28 @@ services/api.ts (1324L)
|
|||||||
|
|
||||||
## P2 — 架构优化
|
## P2 — 架构优化
|
||||||
|
|
||||||
### A1: K8s 双路由体系统一 [pending]
|
### A1: K8s 双路由体系统一 [done]
|
||||||
|
|
||||||
**问题**: 104 条 K8s 路由存在集群域 `/kubernetes-clusters/:id/...` 和 Kuboard 域 `/kubernetes/:code/...` 两套
|
**问题**: 104 条 K8s 路由存在集群域 `/kubernetes-clusters/:id/...` 和 Kuboard 域 `/kubernetes/:code/...` 两套
|
||||||
|
|
||||||
**方案**: 以 Kuboard code 路由为主,ID 路由保留向后兼容(标记 deprecated)
|
**方案**: 以 Kuboard code 路由为主,ID 路由保留向后兼容(标记 deprecated)
|
||||||
|
|
||||||
|
**已完成 (2026-06-27)**:
|
||||||
|
- ✅ 后端新增 4 条缺失的 all-NS code 路由: `services` / `ingresses` / `cronjobs` / `nodes`
|
||||||
|
- ✅ `ListNodes` handler 改用 `getK8sClientByCode`(兼容 ID 和 code 双参数)
|
||||||
|
- ✅ ID 域资源列举路由全部标记 `// Deprecated:` 注释
|
||||||
|
- ✅ 前端 `clusterApi` / `nodeApi` / `workloadApi` 资源方法全部迁移到 code 路由
|
||||||
|
- ✅ 3 个组件 (KubernetesClusterDetail / TopologyView / KubernetesNodes) 从 `cluster.id` 迁移到 `cluster.code`
|
||||||
|
- ✅ 修复潜在 bug: ClusterOverviewPage 曾将 code 传给 ID-based getNodes → strconv.Atoi 返回 0
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- [x] code 路由覆盖所有资源列举场景(7 条 all-NS code 路由)
|
||||||
|
- [x] ID 路由保留向后兼容(标注 Deprecated,功能不变)
|
||||||
|
- [x] `go build` 零错误
|
||||||
|
- [x] `npm run build` 零错误,`tsc -b` 零错误
|
||||||
|
- [x] 16/16 API 测试通过(7 新路由 + 3 旧路由兼容 + 6 回归)
|
||||||
|
- [x] code 路由与 ID 路由返回相同数据(nodes=2, pods=24, deployments=15)
|
||||||
|
|
||||||
### A2: 告警引擎分布式安全 [done]
|
### A2: 告警引擎分布式安全 [done]
|
||||||
|
|
||||||
**问题**: 告警引擎用 goroutine + 30s ticker 常驻,多副本会重复告警
|
**问题**: 告警引擎用 goroutine + 30s ticker 常驻,多副本会重复告警
|
||||||
@@ -214,7 +230,7 @@ services/api.ts (1324L)
|
|||||||
| F1 | 告警静默规则 | P1 | **done** |
|
| F1 | 告警静默规则 | P1 | **done** |
|
||||||
| F2 | 审计日志导出 | P1 | **done** |
|
| F2 | 审计日志导出 | P1 | **done** |
|
||||||
| F3 | 前端 RBAC 控制 | P1 | **done** |
|
| F3 | 前端 RBAC 控制 | P1 | **done** |
|
||||||
| A1 | K8s 路由统一 | P2 | pending |
|
| A1 | K8s 路由统一 | P2 | **done** |
|
||||||
| A2 | 告警引擎分布式 | P2 | **done** |
|
| A2 | 告警引擎分布式 | P2 | **done** |
|
||||||
| A3 | WebSocket 管理 | P2 | pending |
|
| A3 | WebSocket 管理 | P2 | pending |
|
||||||
| U1 | 虚拟滚动 | P3 | pending |
|
| U1 | 虚拟滚动 | P3 | pending |
|
||||||
|
|||||||
+63
@@ -387,6 +387,69 @@ curl -s http://127.0.0.1:8080/api/v1/alerts/engine/status \
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## P2-A1: K8s 双路由体系统一
|
||||||
|
|
||||||
|
### 验证用例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TOKEN=$(python3 -c "
|
||||||
|
import urllib.request, json
|
||||||
|
login_data = json.dumps({'username': 'admin', 'password': '123456'}).encode()
|
||||||
|
req = urllib.request.Request('http://127.0.0.1:8080/api/v1/auth/login', data=login_data, method='POST')
|
||||||
|
req.add_header('Content-Type', 'application/json')
|
||||||
|
resp = urllib.request.urlopen(req)
|
||||||
|
print(json.loads(resp.read())['data']['token'])
|
||||||
|
")
|
||||||
|
|
||||||
|
# U1: 新增 code 路由 — nodes
|
||||||
|
curl -sf "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/nodes" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'nodes: {len(d[\"data\"])}')"
|
||||||
|
# 期望: code=200, nodes 数组
|
||||||
|
|
||||||
|
# U2: 新增 code 路由 — services (A1 新增)
|
||||||
|
curl -sf "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/services" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'services: {len(d[\"data\"])}')"
|
||||||
|
# 期望: code=200, services 数组
|
||||||
|
|
||||||
|
# U3: 新增 code 路由 — ingresses (A1 新增)
|
||||||
|
curl -sf "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/ingresses" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'ingresses: {len(d[\"data\"])}')"
|
||||||
|
# 期望: code=200, ingresses 数组
|
||||||
|
|
||||||
|
# U4: 新增 code 路由 — cronjobs (A1 新增)
|
||||||
|
curl -sf "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/cronjobs" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'cronjobs: {len(d[\"data\"])}')"
|
||||||
|
# 期望: code=200
|
||||||
|
|
||||||
|
# U5: 旧 ID 路由向后兼容 — nodes
|
||||||
|
curl -sf "http://127.0.0.1:8080/api/v1/kubernetes-clusters/1/nodes" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" > /dev/null && echo "✅ ID route still works" || echo "❌ ID route broken"
|
||||||
|
# 期望: 200 OK
|
||||||
|
|
||||||
|
# U6: Go 编译
|
||||||
|
cd /srv/aiops/backend && go build ./...
|
||||||
|
# 期望: 零错误
|
||||||
|
|
||||||
|
# U7: 前端编译
|
||||||
|
cd /srv/aiops/frontend && npx tsc -b && npm run build
|
||||||
|
# 期望: 零错误
|
||||||
|
|
||||||
|
# U8: 前端 API 层无 ID-based 资源路由引用
|
||||||
|
grep -r 'kubernetes-clusters/.*\(pods\|deployments\|services\|ingresses\|nodes\|cronjobs\)' /srv/aiops/frontend/src/api/
|
||||||
|
# 期望: 无输出(所有资源方法已迁移到 code 路由)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 通过标准
|
||||||
|
- [x] U1-U4: 4 条新增/补全的 code 路由全部返回 200 + 正确数据
|
||||||
|
- [x] U5: 旧 ID 路由向后兼容(nodes/pods/deployments/services/ingresses 均可访问)
|
||||||
|
- [x] U6: `go build` 零错误
|
||||||
|
- [x] U7: `tsc -b` + `npm run build` 零错误
|
||||||
|
- [x] U8: 前端 API 层资源方法零 ID 路由引用(仅保留集群 CRUD/sync/topology 合法 ID 路由)
|
||||||
|
- [x] 16/16 综合 API 测试通过(7 新 code 路由 + 3 旧 ID 路由 + 6 回归)
|
||||||
|
- [x] code 路由与 ID 路由返回数据一致(nodes=2, pods=24, deployments=15)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 回归测试基线(每次改动后必跑)
|
## 回归测试基线(每次改动后必跑)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -100,6 +100,27 @@ func (h *KubernetesHandler) ListCronJobs(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, model.Response{Code: 200, Data: cronjobs.Items})
|
c.JSON(http.StatusOK, model.Response{Code: 200, Data: cronjobs.Items})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListCronJobsAllNS lists all CronJobs across all namespaces using clusterCode.
|
||||||
|
// Canonical route: GET /kubernetes/:clusterCode/cronjobs
|
||||||
|
func (h *KubernetesHandler) ListCronJobsAllNS(c *gin.Context) {
|
||||||
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
if clusterID == 0 {
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cronjobs, err := client.ListCronJobs()
|
||||||
|
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: cronjobs.Items})
|
||||||
|
}
|
||||||
|
|
||||||
// CreateCronJob 创建 CronJob
|
// CreateCronJob 创建 CronJob
|
||||||
func (h *KubernetesHandler) CreateCronJob(c *gin.Context) {
|
func (h *KubernetesHandler) CreateCronJob(c *gin.Context) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
|
|||||||
@@ -13,9 +13,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (h *KubernetesHandler) ListNodes(c *gin.Context) {
|
func (h *KubernetesHandler) ListNodes(c *gin.Context) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
// 兼容 /kubernetes-clusters/:id/nodes (ID) 和 /kubernetes/:clusterCode/nodes (code)
|
||||||
|
client, _, err := h.getK8sClientByCode(c)
|
||||||
client, _, err := h.getK8sClient(clusterID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -99,6 +99,94 @@ func (h *KubernetesHandler) ListIngresses(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: result})
|
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: result})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListServicesAllNS lists all Services across all namespaces using clusterCode.
|
||||||
|
// Canonical route: GET /kubernetes/:clusterCode/services
|
||||||
|
func (h *KubernetesHandler) ListServicesAllNS(c *gin.Context) {
|
||||||
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
if clusterID == 0 {
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
namespace := c.Query("namespace")
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
svcs, err := client.ListServices()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
type SvcSummary struct {
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
ClusterIP string `json:"cluster_ip"`
|
||||||
|
Ports int `json:"port_count"`
|
||||||
|
}
|
||||||
|
result := []SvcSummary{}
|
||||||
|
for _, s := range svcs.Items {
|
||||||
|
if namespace != "" && s.Metadata.Namespace != namespace {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result = append(result, SvcSummary{
|
||||||
|
Namespace: s.Metadata.Namespace,
|
||||||
|
Name: s.Metadata.Name,
|
||||||
|
Type: s.Spec.Type,
|
||||||
|
ClusterIP: s.Spec.ClusterIP,
|
||||||
|
Ports: len(s.Spec.Ports),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: result})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListIngressesAllNS lists all Ingresses across all namespaces using clusterCode.
|
||||||
|
// Canonical route: GET /kubernetes/:clusterCode/ingresses
|
||||||
|
func (h *KubernetesHandler) ListIngressesAllNS(c *gin.Context) {
|
||||||
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
if clusterID == 0 {
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
namespace := c.Query("namespace")
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ings, err := client.ListIngresses()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
type IngSummary struct {
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Hosts []string `json:"hosts"`
|
||||||
|
TLS bool `json:"tls"`
|
||||||
|
}
|
||||||
|
result := []IngSummary{}
|
||||||
|
for _, ing := range ings.Items {
|
||||||
|
if namespace != "" && ing.Metadata.Namespace != namespace {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hosts := []string{}
|
||||||
|
for _, rule := range ing.Spec.Rules {
|
||||||
|
if rule.Host != "" {
|
||||||
|
hosts = append(hosts, rule.Host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, IngSummary{
|
||||||
|
Namespace: ing.Metadata.Namespace,
|
||||||
|
Name: ing.Metadata.Name,
|
||||||
|
Hosts: hosts,
|
||||||
|
TLS: len(ing.Spec.TLS) > 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: result})
|
||||||
|
}
|
||||||
|
|
||||||
// GetTopology 获取集群拓扑布局 // Logan.Li
|
// GetTopology 获取集群拓扑布局 // Logan.Li
|
||||||
func (h *KubernetesHandler) GetTopology(c *gin.Context) {
|
func (h *KubernetesHandler) GetTopology(c *gin.Context) {
|
||||||
clusterID := c.Param("id")
|
clusterID := c.Param("id")
|
||||||
|
|||||||
@@ -319,7 +319,11 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
|||||||
resourceModels.DELETE("/:id", resourceModelHandler.DeleteResourceModel)
|
resourceModels.DELETE("/:id", resourceModelHandler.DeleteResourceModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// K8s集群路由
|
// ============================================================================
|
||||||
|
// 集群管理路由 (cluster CRUD + sync + topology — 合法 ID 路由)
|
||||||
|
// 资源列举路由 (nodes/pods/deployments/services/...) 保留向后兼容,
|
||||||
|
// 但应优先使用 /kubernetes/:clusterCode/... (Kuboard code 路由)。详见 ROADMAP A1。
|
||||||
|
// ============================================================================
|
||||||
clusters := authorized.Group("/kubernetes-clusters")
|
clusters := authorized.Group("/kubernetes-clusters")
|
||||||
{
|
{
|
||||||
clusters.GET("", k8sHandler.ListClusters)
|
clusters.GET("", k8sHandler.ListClusters)
|
||||||
@@ -329,12 +333,12 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
|||||||
clusters.PUT("/:id", k8sHandler.UpdateCluster)
|
clusters.PUT("/:id", k8sHandler.UpdateCluster)
|
||||||
clusters.DELETE("/:id", k8sHandler.DeleteCluster)
|
clusters.DELETE("/:id", k8sHandler.DeleteCluster)
|
||||||
clusters.POST("/:id/sync", k8sHandler.SyncCluster) // 同步集群资源 // Logan.Li
|
clusters.POST("/:id/sync", k8sHandler.SyncCluster) // 同步集群资源 // Logan.Li
|
||||||
clusters.GET("/:id/nodes", k8sHandler.ListNodes)
|
clusters.GET("/:id/nodes", k8sHandler.ListNodes) // Deprecated: use GET /kubernetes/:clusterCode/nodes
|
||||||
clusters.GET("/:id/nodes/:name", k8sHandler.GetNodeDetail) // M10 节点详情 // Logan.Li
|
clusters.GET("/:id/nodes/:name", k8sHandler.GetNodeDetail) // M10 节点详情 // Logan.Li
|
||||||
clusters.GET("/:id/pods", k8sHandler.ListPods)
|
clusters.GET("/:id/pods", k8sHandler.ListPods) // Deprecated: use GET /kubernetes/:clusterCode/pods
|
||||||
clusters.GET("/:id/deployments", k8sHandler.ListDeployments)
|
clusters.GET("/:id/deployments", k8sHandler.ListDeployments) // Deprecated: use GET /kubernetes/:clusterCode/deployments
|
||||||
clusters.GET("/:id/services", k8sHandler.ListServices)
|
clusters.GET("/:id/services", k8sHandler.ListServices) // Deprecated: use GET /kubernetes/:clusterCode/services
|
||||||
clusters.GET("/:id/ingresses", k8sHandler.ListIngresses)
|
clusters.GET("/:id/ingresses", k8sHandler.ListIngresses) // Deprecated: use GET /kubernetes/:clusterCode/ingresses
|
||||||
clusters.GET("/:id/topology", k8sHandler.GetTopology) // 获取拓扑布局 // Logan.Li
|
clusters.GET("/:id/topology", k8sHandler.GetTopology) // 获取拓扑布局 // Logan.Li
|
||||||
clusters.PUT("/:id/topology", k8sHandler.SaveTopology) // 保存拓扑布局 // Logan.Li
|
clusters.PUT("/:id/topology", k8sHandler.SaveTopology) // 保存拓扑布局 // Logan.Li
|
||||||
|
|
||||||
@@ -385,6 +389,8 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
|||||||
|
|
||||||
// Node detail (cluster-scoped, not namespace-scoped) // M10
|
// Node detail (cluster-scoped, not namespace-scoped) // M10
|
||||||
kubernetesRouter.GET("/:clusterCode/nodes/:name", k8sHandler.GetNodeDetail)
|
kubernetesRouter.GET("/:clusterCode/nodes/:name", k8sHandler.GetNodeDetail)
|
||||||
|
// Cluster-scoped node list (canonical — replaces /kubernetes-clusters/:id/nodes)
|
||||||
|
kubernetesRouter.GET("/:clusterCode/nodes", k8sHandler.ListNodes)
|
||||||
|
|
||||||
// Deployments
|
// Deployments
|
||||||
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/deployments", k8sHandler.ListDeploymentsNS)
|
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/deployments", k8sHandler.ListDeploymentsNS)
|
||||||
@@ -443,10 +449,13 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
|||||||
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/detail", k8sHandler.GetNamespaceDetailHandler)
|
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/detail", k8sHandler.GetNamespaceDetailHandler)
|
||||||
kubernetesRouter.POST("/:clusterCode/namespaces", k8sHandler.CreateNamespaceHandler)
|
kubernetesRouter.POST("/:clusterCode/namespaces", k8sHandler.CreateNamespaceHandler)
|
||||||
kubernetesRouter.DELETE("/:clusterCode/namespaces/:namespace", k8sHandler.DeleteNamespaceHandler)
|
kubernetesRouter.DELETE("/:clusterCode/namespaces/:namespace", k8sHandler.DeleteNamespaceHandler)
|
||||||
// All-namespace listing (no ns filter)
|
// All-namespace listing (no ns filter) — canonical code-based routes (A1)
|
||||||
kubernetesRouter.GET("/:clusterCode/deployments", k8sHandler.ListDeploymentsAllNS)
|
kubernetesRouter.GET("/:clusterCode/deployments", k8sHandler.ListDeploymentsAllNS)
|
||||||
kubernetesRouter.GET("/:clusterCode/pods", k8sHandler.ListPodsAllNS)
|
kubernetesRouter.GET("/:clusterCode/pods", k8sHandler.ListPodsAllNS)
|
||||||
kubernetesRouter.GET("/:clusterCode/hpas", k8sHandler.ListHPAsAllNS)
|
kubernetesRouter.GET("/:clusterCode/hpas", k8sHandler.ListHPAsAllNS)
|
||||||
|
kubernetesRouter.GET("/:clusterCode/services", k8sHandler.ListServicesAllNS)
|
||||||
|
kubernetesRouter.GET("/:clusterCode/ingresses", k8sHandler.ListIngressesAllNS)
|
||||||
|
kubernetesRouter.GET("/:clusterCode/cronjobs", k8sHandler.ListCronJobsAllNS)
|
||||||
|
|
||||||
// 🆕 Pod Metrics (kubectl top pods)
|
// 🆕 Pod Metrics (kubectl top pods)
|
||||||
kubernetesRouter.GET("/:clusterCode/metrics/pods", k8sHandler.GetPodMetrics)
|
kubernetesRouter.GET("/:clusterCode/metrics/pods", k8sHandler.GetPodMetrics)
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ export const clusterApi = {
|
|||||||
createCluster: async (data: Record<string, unknown>) => http.post('/kubernetes-clusters', data),
|
createCluster: async (data: Record<string, unknown>) => http.post('/kubernetes-clusters', data),
|
||||||
updateCluster: async (id: number | string, data: Record<string, unknown>) => http.put(`/kubernetes-clusters/${id}`, data),
|
updateCluster: async (id: number | string, data: Record<string, unknown>) => http.put(`/kubernetes-clusters/${id}`, data),
|
||||||
deleteCluster: async (id: number | string) => http.delete(`/kubernetes-clusters/${id}`),
|
deleteCluster: async (id: number | string) => http.delete(`/kubernetes-clusters/${id}`),
|
||||||
getPods: async (clusterId: number | string) => http.get(`/kubernetes-clusters/${clusterId}/pods`),
|
// 资源列举 — 统一使用 Kuboard code 路由 (ROADMAP A1)
|
||||||
getDeployments: async (clusterId: number | string) => http.get(`/kubernetes-clusters/${clusterId}/deployments`),
|
getPods: async (clusterCode: string) => http.get(`/kubernetes/${clusterCode}/pods`),
|
||||||
getServices: async (clusterId: number | string) => http.get(`/kubernetes-clusters/${clusterId}/services`),
|
getDeployments: async (clusterCode: string) => http.get(`/kubernetes/${clusterCode}/deployments`),
|
||||||
getIngresses: async (clusterId: number | string) => http.get(`/kubernetes-clusters/${clusterId}/ingresses`),
|
getServices: async (clusterCode: string) => http.get(`/kubernetes/${clusterCode}/services`),
|
||||||
|
getIngresses: async (clusterCode: string) => http.get(`/kubernetes/${clusterCode}/ingresses`),
|
||||||
syncCluster: async (clusterId: number | string) => http.post(`/kubernetes-clusters/${clusterId}/sync`),
|
syncCluster: async (clusterId: number | string) => http.post(`/kubernetes-clusters/${clusterId}/sync`),
|
||||||
exportAllResources: async (clusterCode: string) =>
|
exportAllResources: async (clusterCode: string) =>
|
||||||
http.download(`/kubernetes/${clusterCode}/export`, `cluster-${clusterCode}-resources.tar.gz`),
|
http.download(`/kubernetes/${clusterCode}/export`, `cluster-${clusterCode}-resources.tar.gz`),
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
import { http } from '@/utils/request';
|
import { http } from '@/utils/request';
|
||||||
|
|
||||||
export const nodeApi = {
|
export const nodeApi = {
|
||||||
getNodes: async (clusterId: number | string) => http.get(`/kubernetes-clusters/${clusterId}/nodes`),
|
// 统一使用 Kuboard code 路由 (ROADMAP A1)
|
||||||
|
getNodes: async (clusterCode: string) => http.get(`/kubernetes/${clusterCode}/nodes`),
|
||||||
|
// 别名 — 保持向后兼容(旧代码可能调用 getClusterNodes)
|
||||||
getClusterNodes: async (clusterCode: string) => http.get(`/kubernetes/${clusterCode}/nodes`),
|
getClusterNodes: async (clusterCode: string) => http.get(`/kubernetes/${clusterCode}/nodes`),
|
||||||
getNodeDetail: async (clusterCode: string, nodeName: string) =>
|
getNodeDetail: async (clusterCode: string, nodeName: string) =>
|
||||||
http.get(`/kubernetes/${clusterCode}/nodes/${encodeURIComponent(nodeName)}`),
|
http.get(`/kubernetes/${clusterCode}/nodes/${encodeURIComponent(nodeName)}`),
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const workloadApi = {
|
|||||||
http.delete(`/kubernetes/${clusterCode}/namespace/${namespace}/hpas/${name}`),
|
http.delete(`/kubernetes/${clusterCode}/namespace/${namespace}/hpas/${name}`),
|
||||||
getCronJobs: async (clusterCode: string, namespace?: string) => {
|
getCronJobs: async (clusterCode: string, namespace?: string) => {
|
||||||
if (namespace) return http.get(`/kubernetes/${clusterCode}/namespace/${namespace}/cronjobs`);
|
if (namespace) return http.get(`/kubernetes/${clusterCode}/namespace/${namespace}/cronjobs`);
|
||||||
return http.get(`/kubernetes-clusters/${clusterCode}/cronjobs`);
|
return http.get(`/kubernetes/${clusterCode}/cronjobs`);
|
||||||
},
|
},
|
||||||
createCronJob: async (
|
createCronJob: async (
|
||||||
clusterCode: string,
|
clusterCode: string,
|
||||||
|
|||||||
@@ -67,15 +67,16 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
|||||||
];
|
];
|
||||||
}, [clusterMetrics]);
|
}, [clusterMetrics]);
|
||||||
|
|
||||||
const loadClusterDetail = async (clusterId: string) => {
|
const loadClusterDetail = async (clusterCode: string) => {
|
||||||
try {
|
try {
|
||||||
// 后端 ListNodes/ListPods/etc 返回 data 直接为数组(非 {list,total})
|
// 后端 ListNodes/ListPods/etc 返回 data 直接为数组(非 {list,total})
|
||||||
|
// 统一使用 Kuboard code 路由 (ROADMAP A1)
|
||||||
const [nodesRes, podsRes, depRes, svcRes, ingRes] = await Promise.all([
|
const [nodesRes, podsRes, depRes, svcRes, ingRes] = await Promise.all([
|
||||||
kubernetesApi.getNodes(clusterId).catch(() => []),
|
kubernetesApi.getNodes(clusterCode).catch(() => []),
|
||||||
kubernetesApi.getPods(clusterId).catch(() => []),
|
kubernetesApi.getPods(clusterCode).catch(() => []),
|
||||||
kubernetesApi.getDeployments(clusterId).catch(() => []),
|
kubernetesApi.getDeployments(clusterCode).catch(() => []),
|
||||||
kubernetesApi.getServices(clusterId).catch(() => []),
|
kubernetesApi.getServices(clusterCode).catch(() => []),
|
||||||
kubernetesApi.getIngresses(clusterId).catch(() => []),
|
kubernetesApi.getIngresses(clusterCode).catch(() => []),
|
||||||
]);
|
]);
|
||||||
const toArray = (v: JsonData) => (Array.isArray(v) ? v : (v?.list || []));
|
const toArray = (v: JsonData) => (Array.isArray(v) ? v : (v?.list || []));
|
||||||
setNodes(toArray(nodesRes));
|
setNodes(toArray(nodesRes));
|
||||||
@@ -89,10 +90,10 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (cluster?.id) {
|
if (cluster?.code) {
|
||||||
loadClusterDetail(cluster.id);
|
loadClusterDetail(cluster.code);
|
||||||
}
|
}
|
||||||
}, [cluster?.id]);
|
}, [cluster?.code]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-slate-900 border-slate-800">
|
<Card className="bg-slate-900 border-slate-800">
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function KubernetesNodes() {
|
|||||||
const res: JsonData = await kubernetesApi.getClusters();
|
const res: JsonData = await kubernetesApi.getClusters();
|
||||||
const list = res?.list || [];
|
const list = res?.list || [];
|
||||||
setClusters(list);
|
setClusters(list);
|
||||||
if (list.length > 0 && !selectedCluster) setSelectedCluster(String(list[0].id));
|
if (list.length > 0 && !selectedCluster) setSelectedCluster(String(list[0].code || list[0].id));
|
||||||
} catch { setClusters([]); }
|
} catch { setClusters([]); }
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ export function KubernetesNodes() {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="bg-slate-800 border-slate-700">
|
<SelectContent className="bg-slate-800 border-slate-700">
|
||||||
{clusters.map((c) => (
|
{clusters.map((c) => (
|
||||||
<SelectItem key={c.id} value={String(c.id)}>{c.name}</SelectItem>
|
<SelectItem key={c.id} value={String(c.code || c.id)}>{c.name}</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -96,14 +96,16 @@ export function TopologyView() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const toArray = ( v: any) => (Array.isArray(v) ? v : (v?.list || []));
|
const toArray = ( v: any) => (Array.isArray(v) ? v : (v?.list || []));
|
||||||
const [nodesRes, svcRes, depRes, ingRes, podRes] = await Promise.all([
|
|
||||||
kubernetesApi.getNodes(selectedClusterId).catch(() => []),
|
|
||||||
kubernetesApi.getServices(selectedClusterId).catch(() => []),
|
|
||||||
kubernetesApi.getDeployments(selectedClusterId).catch(() => []),
|
|
||||||
kubernetesApi.getIngresses(selectedClusterId).catch(() => []),
|
|
||||||
kubernetesApi.getPods(selectedClusterId).catch(() => []),
|
|
||||||
]);
|
|
||||||
const cluster = clusters.find((c) => String(c.id) === selectedClusterId) || {};
|
const cluster = clusters.find((c) => String(c.id) === selectedClusterId) || {};
|
||||||
|
// 统一使用 Kuboard code 路由获取资源 (ROADMAP A1)
|
||||||
|
const clusterCode = (cluster as any).code || selectedClusterId;
|
||||||
|
const [nodesRes, svcRes, depRes, ingRes, podRes] = await Promise.all([
|
||||||
|
kubernetesApi.getNodes(clusterCode).catch(() => []),
|
||||||
|
kubernetesApi.getServices(clusterCode).catch(() => []),
|
||||||
|
kubernetesApi.getDeployments(clusterCode).catch(() => []),
|
||||||
|
kubernetesApi.getIngresses(clusterCode).catch(() => []),
|
||||||
|
kubernetesApi.getPods(clusterCode).catch(() => []),
|
||||||
|
]);
|
||||||
const built = buildTopology(cluster, toArray(ingRes), toArray(svcRes), toArray(depRes), toArray(nodesRes), toArray(podRes));
|
const built = buildTopology(cluster, toArray(ingRes), toArray(svcRes), toArray(depRes), toArray(nodesRes), toArray(podRes));
|
||||||
|
|
||||||
// 合并已保存布局
|
// 合并已保存布局
|
||||||
|
|||||||
Reference in new issue
Block a user