From 00a98e210d437d6364cce0403bbf77e652dbe3ad Mon Sep 17 00:00:00 2001 From: attacker Date: Sat, 27 Jun 2026 04:50:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(k8s):=20A1=20K8s=20=E5=8F=8C=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E4=BD=93=E7=B3=BB=E7=BB=9F=E4=B8=80=20=E2=80=94=20cod?= =?UTF-8?q?e=20=E8=B7=AF=E7=94=B1=E4=B8=BA=E4=B8=BB,=20ID=20=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E6=A0=87=E8=AE=B0=20deprecated?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端: - 新增 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=✅ --- PROJECT_EVALUATION.md | 4 +- ROADMAP.md | 20 ++++- TDD_PLAN.md | 63 +++++++++++++ backend/controller/k8s_hpa_cronjob.go | 21 +++++ backend/controller/k8s_node.go | 5 +- backend/controller/k8s_resource.go | 88 +++++++++++++++++++ backend/router/routes.go | 25 ++++-- frontend/src/api/kubernetes/cluster.ts | 9 +- frontend/src/api/kubernetes/node.ts | 4 +- frontend/src/api/kubernetes/workload.ts | 2 +- .../kubernetes/KubernetesClusterDetail.tsx | 19 ++-- .../components/kubernetes/KubernetesNodes.tsx | 4 +- .../components/kubernetes/TopologyView.tsx | 16 ++-- 13 files changed, 241 insertions(+), 39 deletions(-) diff --git a/PROJECT_EVALUATION.md b/PROJECT_EVALUATION.md index 4fc3850..713131c 100644 --- a/PROJECT_EVALUATION.md +++ b/PROJECT_EVALUATION.md @@ -125,7 +125,7 @@ | ~~`kubernetes.go` 4204 行/134 函数~~ → 已拆分为 12 个域文件(最大 696 行) | ✅ 已解决 | 后端 | ~~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) | -| K8s 双路由体系(集群域+Kuboard 域) | 路由冗余 104 条 | 后端 | P1 | +| ~~K8s 双路由体系(集群域+Kuboard 域)~~ → code 路由统一为主,ID 路由标记 deprecated 向后兼容 | ✅ 已解决 | 后端 | ~~P1~~ done | | 25 个 ≥500 行巨型组件 | 拆分困难 | 前端 | P1 | ### 3.2 🟡 中优先级 @@ -201,7 +201,7 @@ |------|:----:|------| | 功能完整性 | **88/100** | 8 大模块齐全,K8s 管理极为完善;扣分项:监控大屏/链路追踪/告警静默未对接 | | 代码质量 | **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 统一入口;扣分项:后端镜像偏大 | | 用户体验 | **86/100** | 骨架屏 + 页面过渡 + 5 套主题 + 懒加载;扣分项:无 i18n,无虚拟滚动 | | **综合** | **85/100** | 企业级运维平台,功能覆盖全面,工程治理 P0 全部完成 | diff --git a/ROADMAP.md b/ROADMAP.md index adda796..acbabbd 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -156,12 +156,28 @@ services/api.ts (1324L) ## P2 — 架构优化 -### A1: K8s 双路由体系统一 [pending] +### A1: K8s 双路由体系统一 [done] **问题**: 104 条 K8s 路由存在集群域 `/kubernetes-clusters/:id/...` 和 Kuboard 域 `/kubernetes/:code/...` 两套 **方案**: 以 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] **问题**: 告警引擎用 goroutine + 30s ticker 常驻,多副本会重复告警 @@ -214,7 +230,7 @@ services/api.ts (1324L) | F1 | 告警静默规则 | P1 | **done** | | F2 | 审计日志导出 | P1 | **done** | | F3 | 前端 RBAC 控制 | P1 | **done** | -| A1 | K8s 路由统一 | P2 | pending | +| A1 | K8s 路由统一 | P2 | **done** | | A2 | 告警引擎分布式 | P2 | **done** | | A3 | WebSocket 管理 | P2 | pending | | U1 | 虚拟滚动 | P3 | pending | diff --git a/TDD_PLAN.md b/TDD_PLAN.md index 9026ed8..6f81997 100644 --- a/TDD_PLAN.md +++ b/TDD_PLAN.md @@ -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 diff --git a/backend/controller/k8s_hpa_cronjob.go b/backend/controller/k8s_hpa_cronjob.go index 5b2ccbf..b0006f4 100644 --- a/backend/controller/k8s_hpa_cronjob.go +++ b/backend/controller/k8s_hpa_cronjob.go @@ -100,6 +100,27 @@ func (h *KubernetesHandler) ListCronJobs(c *gin.Context) { 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 func (h *KubernetesHandler) CreateCronJob(c *gin.Context) { clusterID, _ := strconv.Atoi(c.Param("id")) diff --git a/backend/controller/k8s_node.go b/backend/controller/k8s_node.go index d22aa9a..3cda9ee 100644 --- a/backend/controller/k8s_node.go +++ b/backend/controller/k8s_node.go @@ -13,9 +13,8 @@ import ( ) func (h *KubernetesHandler) ListNodes(c *gin.Context) { - clusterID, _ := strconv.Atoi(c.Param("id")) - - client, _, err := h.getK8sClient(clusterID) + // 兼容 /kubernetes-clusters/:id/nodes (ID) 和 /kubernetes/:clusterCode/nodes (code) + client, _, err := h.getK8sClientByCode(c) if err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()}) return diff --git a/backend/controller/k8s_resource.go b/backend/controller/k8s_resource.go index f6a5f15..0590ecd 100644 --- a/backend/controller/k8s_resource.go +++ b/backend/controller/k8s_resource.go @@ -99,6 +99,94 @@ func (h *KubernetesHandler) ListIngresses(c *gin.Context) { 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 func (h *KubernetesHandler) GetTopology(c *gin.Context) { clusterID := c.Param("id") diff --git a/backend/router/routes.go b/backend/router/routes.go index a19c86e..303d78f 100644 --- a/backend/router/routes.go +++ b/backend/router/routes.go @@ -319,7 +319,11 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c 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.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.DELETE("/:id", k8sHandler.DeleteCluster) clusters.POST("/:id/sync", k8sHandler.SyncCluster) // 同步集群资源 // Logan.Li - clusters.GET("/:id/nodes", k8sHandler.ListNodes) - clusters.GET("/:id/nodes/:name", k8sHandler.GetNodeDetail) // M10 节点详情 // Logan.Li - clusters.GET("/:id/pods", k8sHandler.ListPods) - clusters.GET("/:id/deployments", k8sHandler.ListDeployments) - clusters.GET("/:id/services", k8sHandler.ListServices) - clusters.GET("/:id/ingresses", k8sHandler.ListIngresses) + 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/pods", k8sHandler.ListPods) // Deprecated: use GET /kubernetes/:clusterCode/pods + clusters.GET("/:id/deployments", k8sHandler.ListDeployments) // Deprecated: use GET /kubernetes/:clusterCode/deployments + clusters.GET("/:id/services", k8sHandler.ListServices) // Deprecated: use GET /kubernetes/:clusterCode/services + clusters.GET("/:id/ingresses", k8sHandler.ListIngresses) // Deprecated: use GET /kubernetes/:clusterCode/ingresses clusters.GET("/:id/topology", k8sHandler.GetTopology) // 获取拓扑布局 // 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 kubernetesRouter.GET("/:clusterCode/nodes/:name", k8sHandler.GetNodeDetail) + // Cluster-scoped node list (canonical — replaces /kubernetes-clusters/:id/nodes) + kubernetesRouter.GET("/:clusterCode/nodes", k8sHandler.ListNodes) // Deployments 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.POST("/:clusterCode/namespaces", k8sHandler.CreateNamespaceHandler) 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/pods", k8sHandler.ListPodsAllNS) 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) kubernetesRouter.GET("/:clusterCode/metrics/pods", k8sHandler.GetPodMetrics) diff --git a/frontend/src/api/kubernetes/cluster.ts b/frontend/src/api/kubernetes/cluster.ts index 868c7d7..fe6eebb 100644 --- a/frontend/src/api/kubernetes/cluster.ts +++ b/frontend/src/api/kubernetes/cluster.ts @@ -8,10 +8,11 @@ export const clusterApi = { createCluster: async (data: Record) => http.post('/kubernetes-clusters', data), updateCluster: async (id: number | string, data: Record) => http.put(`/kubernetes-clusters/${id}`, data), deleteCluster: async (id: number | string) => http.delete(`/kubernetes-clusters/${id}`), - getPods: async (clusterId: number | string) => http.get(`/kubernetes-clusters/${clusterId}/pods`), - getDeployments: async (clusterId: number | string) => http.get(`/kubernetes-clusters/${clusterId}/deployments`), - getServices: async (clusterId: number | string) => http.get(`/kubernetes-clusters/${clusterId}/services`), - getIngresses: async (clusterId: number | string) => http.get(`/kubernetes-clusters/${clusterId}/ingresses`), + // 资源列举 — 统一使用 Kuboard code 路由 (ROADMAP A1) + getPods: async (clusterCode: string) => http.get(`/kubernetes/${clusterCode}/pods`), + getDeployments: async (clusterCode: string) => http.get(`/kubernetes/${clusterCode}/deployments`), + 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`), exportAllResources: async (clusterCode: string) => http.download(`/kubernetes/${clusterCode}/export`, `cluster-${clusterCode}-resources.tar.gz`), diff --git a/frontend/src/api/kubernetes/node.ts b/frontend/src/api/kubernetes/node.ts index 9368216..bc7d240 100644 --- a/frontend/src/api/kubernetes/node.ts +++ b/frontend/src/api/kubernetes/node.ts @@ -3,7 +3,9 @@ import { http } from '@/utils/request'; 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`), getNodeDetail: async (clusterCode: string, nodeName: string) => http.get(`/kubernetes/${clusterCode}/nodes/${encodeURIComponent(nodeName)}`), diff --git a/frontend/src/api/kubernetes/workload.ts b/frontend/src/api/kubernetes/workload.ts index d3d6b9c..965c91a 100644 --- a/frontend/src/api/kubernetes/workload.ts +++ b/frontend/src/api/kubernetes/workload.ts @@ -42,7 +42,7 @@ export const workloadApi = { http.delete(`/kubernetes/${clusterCode}/namespace/${namespace}/hpas/${name}`), getCronJobs: async (clusterCode: string, namespace?: string) => { 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 ( clusterCode: string, diff --git a/frontend/src/components/kubernetes/KubernetesClusterDetail.tsx b/frontend/src/components/kubernetes/KubernetesClusterDetail.tsx index c48b91b..bd646ca 100644 --- a/frontend/src/components/kubernetes/KubernetesClusterDetail.tsx +++ b/frontend/src/components/kubernetes/KubernetesClusterDetail.tsx @@ -67,15 +67,16 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa ]; }, [clusterMetrics]); - const loadClusterDetail = async (clusterId: string) => { + const loadClusterDetail = async (clusterCode: string) => { try { // 后端 ListNodes/ListPods/etc 返回 data 直接为数组(非 {list,total}) + // 统一使用 Kuboard code 路由 (ROADMAP A1) const [nodesRes, podsRes, depRes, svcRes, ingRes] = await Promise.all([ - kubernetesApi.getNodes(clusterId).catch(() => []), - kubernetesApi.getPods(clusterId).catch(() => []), - kubernetesApi.getDeployments(clusterId).catch(() => []), - kubernetesApi.getServices(clusterId).catch(() => []), - kubernetesApi.getIngresses(clusterId).catch(() => []), + kubernetesApi.getNodes(clusterCode).catch(() => []), + kubernetesApi.getPods(clusterCode).catch(() => []), + kubernetesApi.getDeployments(clusterCode).catch(() => []), + kubernetesApi.getServices(clusterCode).catch(() => []), + kubernetesApi.getIngresses(clusterCode).catch(() => []), ]); const toArray = (v: JsonData) => (Array.isArray(v) ? v : (v?.list || [])); setNodes(toArray(nodesRes)); @@ -89,10 +90,10 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa }; useEffect(() => { - if (cluster?.id) { - loadClusterDetail(cluster.id); + if (cluster?.code) { + loadClusterDetail(cluster.code); } - }, [cluster?.id]); + }, [cluster?.code]); return ( diff --git a/frontend/src/components/kubernetes/KubernetesNodes.tsx b/frontend/src/components/kubernetes/KubernetesNodes.tsx index 602c132..90f50df 100644 --- a/frontend/src/components/kubernetes/KubernetesNodes.tsx +++ b/frontend/src/components/kubernetes/KubernetesNodes.tsx @@ -26,7 +26,7 @@ export function KubernetesNodes() { const res: JsonData = await kubernetesApi.getClusters(); const list = res?.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([]); } }, []); @@ -72,7 +72,7 @@ export function KubernetesNodes() { {clusters.map((c) => ( - {c.name} + {c.name} ))} diff --git a/frontend/src/components/kubernetes/TopologyView.tsx b/frontend/src/components/kubernetes/TopologyView.tsx index aeac21d..e0f8fe1 100644 --- a/frontend/src/components/kubernetes/TopologyView.tsx +++ b/frontend/src/components/kubernetes/TopologyView.tsx @@ -96,14 +96,16 @@ export function TopologyView() { setLoading(true); try { 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) || {}; + // 统一使用 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)); // 合并已保存布局