feat(k8s): M11 StatefulSet/DaemonSet 详情页 — WorkloadViewPage 重写 + YAML 路由冲突修复

后端:
- routes.go: 为 statefulsets/daemonsets 注册专门 YAML 子路由(Gin radix tree 冲突)
- kubernetes.go: GetResourceYAMLNS/UpdateResourceYAMLNS 添加 URL path fallback

前端:
- WorkloadViewPage.tsx 重写: 修复 kind 提取 bug(useLocation) + 更新策略/选择器/PVC模板/InitContainers
- NamespaceResourcePage.tsx: handleDetail 添加 STS/DS 点击导航

验证: DS YAML API 200 ✅ | DS Detail 200 ✅ | Dep YAML 200 ✅ | SPA路由 200 ✅ | TS零错误 ✅
This commit is contained in:
attacker committed 2026-06-24 00:39:51 +08:00
1 parent 87b7755094
commit d1c557bb6c
14 files changed
+1103 -178

No files matched your search

+166
View File
@@ -1830,6 +1830,35 @@ func (h *KubernetesHandler) getK8sClientByCode(c *gin.Context) (*k8sclient.Clien
func (h *KubernetesHandler) ListDeploymentsNS(c *gin.Context) {
h.listDeploymentsWithNS(c, c.Param("namespace"))
}
// GetDeploymentNS 获取单个 Deployment 详情(避免前端 404)// Logan.Li
func (h *KubernetesHandler) GetDeploymentNS(c *gin.Context) {
ns := c.Param("namespace")
name := c.Param("name")
clusterID, _ := strconv.Atoi(c.Param("id"))
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
client, _, err := h.getK8sClient(clusterID)
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
deps, err := client.ListDeployments()
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
for _, d := range deps.Items {
if d.Metadata.Namespace == ns && d.Metadata.Name == name {
image := ""
if len(d.Spec.Template.Spec.Containers) > 0 { image = d.Spec.Template.Spec.Containers[0].Image }
c.JSON(200, models.Response{Code: 200, Data: map[string]interface{}{
"namespace": d.Metadata.Namespace,
"name": d.Metadata.Name,
"replicas": d.Spec.Replicas,
"available_replicas": d.Status.AvailableReplicas,
"ready_replicas": d.Status.ReadyReplicas,
"image": image,
"paused": d.Spec.Paused,
}})
return
}
}
c.JSON(404, models.Response{Code: 404, Message: "Deployment not found"})
}
func (h *KubernetesHandler) ListPodsNS(c *gin.Context) {
h.listPodsWithNS(c, c.Param("namespace"))
}
@@ -2504,6 +2533,16 @@ func (h *KubernetesHandler) GetResourceYAMLNS(c *gin.Context) {
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
ns := c.Param("namespace")
resource := c.Param("resource")
// Fallback: specific routes (statefulsets/:name/yaml, daemonsets/:name/yaml) have no :resource param
if resource == "" {
pathParts := strings.Split(c.Request.URL.Path, "/")
for _, p := range pathParts {
if p == "statefulsets" || p == "daemonsets" || p == "deployments" || p == "services" || p == "configmaps" || p == "secrets" {
resource = p
break
}
}
}
name := c.Param("name")
// Build API path based on resource type
var apiPath string
@@ -2549,6 +2588,16 @@ func (h *KubernetesHandler) UpdateResourceYAMLNS(c *gin.Context) {
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
ns := c.Param("namespace")
resource := c.Param("resource")
// Fallback: specific routes (statefulsets/:name/yaml, daemonsets/:name/yaml) have no :resource param
if resource == "" {
pathParts := strings.Split(c.Request.URL.Path, "/")
for _, p := range pathParts {
if p == "statefulsets" || p == "daemonsets" || p == "deployments" || p == "services" || p == "configmaps" || p == "secrets" {
resource = p
break
}
}
}
name := c.Param("name")
yamlData, err := io.ReadAll(c.Request.Body)
if err != nil { c.JSON(400, models.Response{Code: 400, Message: "读取body失败"}); return }
@@ -2768,6 +2817,123 @@ func (h *KubernetesHandler) ListDaemonSetsNS(c *gin.Context) {
}
c.JSON(200, models.Response{Code: 200, Data: result})
}
// ScaleStatefulSetNS scales a StatefulSet (Kuboard code route)
func (h *KubernetesHandler) ScaleStatefulSetNS(c *gin.Context) {
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
client, _, err := h.getK8sClient(clusterID)
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
ns := c.Param("namespace")
name := c.Param("name")
var req struct{ Replicas int `json:"replicas"` }
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, models.Response{Code: 400, Message: "参数错误"}); return }
if err := client.ScaleStatefulSet(ns, name, req.Replicas); err != nil {
c.JSON(500, models.Response{Code: 500, Message: "扩缩容失败: " + err.Error()}); return
}
c.JSON(200, models.Response{Code: 200, Message: "StatefulSet 副本数已更新为 " + strconv.Itoa(req.Replicas)})
}
// ScaleStatefulSet scales a StatefulSet (ID route)
func (h *KubernetesHandler) ScaleStatefulSet(c *gin.Context) {
clusterID, _ := strconv.Atoi(c.Param("id"))
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
client, _, err := h.getK8sClient(clusterID)
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
ns := c.Param("namespace")
name := c.Param("name")
var req struct{ Replicas int `json:"replicas"` }
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, models.Response{Code: 400, Message: "参数错误"}); return }
if err := client.ScaleStatefulSet(ns, name, req.Replicas); err != nil {
c.JSON(500, models.Response{Code: 500, Message: "扩缩容失败: " + err.Error()}); return
}
c.JSON(200, models.Response{Code: 200, Message: "StatefulSet 副本数已更新为 " + strconv.Itoa(req.Replicas)})
}
// RestartStatefulSetNS restarts a StatefulSet (Kuboard code route)
func (h *KubernetesHandler) RestartStatefulSetNS(c *gin.Context) {
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
client, _, err := h.getK8sClient(clusterID)
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
ns := c.Param("namespace")
name := c.Param("name")
if err := client.RestartStatefulSet(ns, name); err != nil {
c.JSON(500, models.Response{Code: 500, Message: "重启失败: " + err.Error()}); return
}
c.JSON(200, models.Response{Code: 200, Message: "StatefulSet 滚动重启已触发"})
}
// RestartStatefulSet restarts a StatefulSet (ID route)
func (h *KubernetesHandler) RestartStatefulSet(c *gin.Context) {
clusterID, _ := strconv.Atoi(c.Param("id"))
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
client, _, err := h.getK8sClient(clusterID)
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
ns := c.Param("namespace")
name := c.Param("name")
if err := client.RestartStatefulSet(ns, name); err != nil {
c.JSON(500, models.Response{Code: 500, Message: "重启失败: " + err.Error()}); return
}
c.JSON(200, models.Response{Code: 200, Message: "StatefulSet 滚动重启已触发"})
}
// RestartDaemonSetNS restarts a DaemonSet (Kuboard code route)
func (h *KubernetesHandler) RestartDaemonSetNS(c *gin.Context) {
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
client, _, err := h.getK8sClient(clusterID)
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
ns := c.Param("namespace")
name := c.Param("name")
if err := client.RestartDaemonSet(ns, name); err != nil {
c.JSON(500, models.Response{Code: 500, Message: "重启失败: " + err.Error()}); return
}
c.JSON(200, models.Response{Code: 200, Message: "DaemonSet 滚动重启已触发"})
}
// RestartDaemonSet restarts a DaemonSet (ID route)
func (h *KubernetesHandler) RestartDaemonSet(c *gin.Context) {
clusterID, _ := strconv.Atoi(c.Param("id"))
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
client, _, err := h.getK8sClient(clusterID)
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
ns := c.Param("namespace")
name := c.Param("name")
if err := client.RestartDaemonSet(ns, name); err != nil {
c.JSON(500, models.Response{Code: 500, Message: "重启失败: " + err.Error()}); return
}
c.JSON(200, models.Response{Code: 200, Message: "DaemonSet 滚动重启已触发"})
}
// GetStatefulSetDetailNS returns a single StatefulSet detail (Kuboard code route)
func (h *KubernetesHandler) GetStatefulSetDetailNS(c *gin.Context) {
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
client, _, err := h.getK8sClient(clusterID)
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
ns := c.Param("namespace")
name := c.Param("name")
sts, err := client.GetStatefulSet(ns, name)
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
c.JSON(200, models.Response{Code: 200, Data: sts})
}
// GetDaemonSetDetailNS returns a single DaemonSet detail (Kuboard code route)
func (h *KubernetesHandler) GetDaemonSetDetailNS(c *gin.Context) {
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
client, _, err := h.getK8sClient(clusterID)
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
ns := c.Param("namespace")
name := c.Param("name")
ds, err := client.GetDaemonSet(ns, name)
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
c.JSON(200, models.Response{Code: 200, Data: ds})
}
// ============================================================================
// Pod Metrics (kubectl top pods) — per-container CPU/Memory usage via metrics-server
// ============================================================================
+15 -1
View File
@@ -342,6 +342,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
// Deployments
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/deployments", k8sHandler.ListDeploymentsNS)
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/deployments/:name", k8sHandler.GetDeploymentNS)
kubernetesRouter.POST("/:clusterCode/namespace/:namespace/deployments/:name/scale", k8sHandler.ScaleDeploymentNS)
kubernetesRouter.POST("/:clusterCode/namespace/:namespace/deployments/:name/restart", k8sHandler.RestartDeploymentNS)
kubernetesRouter.POST("/:clusterCode/namespace/:namespace/deployments/:name/pause", k8sHandler.PauseDeploymentNS)
@@ -412,8 +413,21 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
// StatefulSet / DaemonSet
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/statefulsets", k8sHandler.ListStatefulSetsNS)
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/daemonsets", k8sHandler.ListDaemonSetsNS)
// StatefulSet operations (Kuboard code routes)
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/statefulsets/:name", k8sHandler.GetStatefulSetDetailNS)
kubernetesRouter.POST("/:clusterCode/namespace/:namespace/statefulsets/:name/scale", k8sHandler.ScaleStatefulSetNS)
kubernetesRouter.POST("/:clusterCode/namespace/:namespace/statefulsets/:name/restart", k8sHandler.RestartStatefulSetNS)
// DaemonSet operations (Kuboard code routes)
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/daemonsets/:name", k8sHandler.GetDaemonSetDetailNS)
kubernetesRouter.POST("/:clusterCode/namespace/:namespace/daemonsets/:name/restart", k8sHandler.RestartDaemonSetNS)
// YAML view/edit
// StatefulSet/DaemonSet YAML (specific routes to avoid Gin radix tree conflict with generic /:resource/:name/yaml)
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/statefulsets/:name/yaml", k8sHandler.GetResourceYAMLNS)
kubernetesRouter.PUT("/:clusterCode/namespace/:namespace/statefulsets/:name/yaml", k8sHandler.UpdateResourceYAMLNS)
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/daemonsets/:name/yaml", k8sHandler.GetResourceYAMLNS)
kubernetesRouter.PUT("/:clusterCode/namespace/:namespace/daemonsets/:name/yaml", k8sHandler.UpdateResourceYAMLNS)
// YAML view/edit (generic — works for resources without specific static routes)
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/:resource/:name/yaml", k8sHandler.GetResourceYAMLNS)
kubernetesRouter.PUT("/:clusterCode/namespace/:namespace/:resource/:name/yaml", k8sHandler.UpdateResourceYAMLNS)
kubernetesRouter.DELETE("/:clusterCode/namespace/:namespace/:resource/:name/yaml", k8sHandler.DeleteResourceYAMLNS)
+69
View File
@@ -1205,6 +1205,75 @@ func (c *Client) ListDaemonSets() (*DaemonSetList, error) {
return &result, nil
}
// ScaleStatefulSet scales a StatefulSet to the given replica count
func (c *Client) ScaleStatefulSet(namespace, name string, replicas int) error {
body := map[string]interface{}{
"spec": map[string]interface{}{
"replicas": replicas,
},
}
path := fmt.Sprintf("/apis/apps/v1/namespaces/%s/statefulsets/%s/scale", namespace, name)
return c.patch(path, body, nil)
}
// RestartStatefulSet triggers a rolling restart of a StatefulSet
func (c *Client) RestartStatefulSet(namespace, name string) error {
body := map[string]interface{}{
"spec": map[string]interface{}{
"template": map[string]interface{}{
"metadata": map[string]interface{}{
"annotations": map[string]string{
"kubectl.kubernetes.io/restartedAt": time.Now().UTC().Format(time.RFC3339),
},
},
},
},
}
path := fmt.Sprintf("/apis/apps/v1/namespaces/%s/statefulsets/%s", namespace, name)
return c.patch(path, body, nil)
}
// RestartDaemonSet triggers a rolling restart of a DaemonSet
func (c *Client) RestartDaemonSet(namespace, name string) error {
body := map[string]interface{}{
"spec": map[string]interface{}{
"template": map[string]interface{}{
"metadata": map[string]interface{}{
"annotations": map[string]string{
"kubectl.kubernetes.io/restartedAt": time.Now().UTC().Format(time.RFC3339),
},
},
},
},
}
path := fmt.Sprintf("/apis/apps/v1/namespaces/%s/daemonsets/%s", namespace, name)
return c.patch(path, body, nil)
}
// GetStatefulSet fetches a single StatefulSet by name
func (c *Client) GetStatefulSet(namespace, name string) (*StatefulSetItem, error) {
var result struct {
StatefulSetItem
}
path := fmt.Sprintf("/apis/apps/v1/namespaces/%s/statefulsets/%s", namespace, name)
if err := c.get(path, &result); err != nil {
return nil, err
}
return &result.StatefulSetItem, nil
}
// GetDaemonSet fetches a single DaemonSet by name
func (c *Client) GetDaemonSet(namespace, name string) (*DaemonSetItem, error) {
var result struct {
DaemonSetItem
}
path := fmt.Sprintf("/apis/apps/v1/namespaces/%s/daemonsets/%s", namespace, name)
if err := c.get(path, &result); err != nil {
return nil, err
}
return &result.DaemonSetItem, nil
}
// ============================================================================
// NodeMetrics (metrics-server, optional)
// ============================================================================