feat(k8s): M5 Deployment 暂停更新 + CI/CD 集成
- 后端: PauseDeployment/ResumeDeployment 客户端方法 (PATCH spec.paused) - 后端: ID路由 + Kuboard code路由 pause/resume handlers - 后端: Deployment 列表 summary 增加 paused 字段 - 前端: 操作菜单增加「暂停更新/恢复更新」动态切换 + 暂停 Badge - 前端: CI/CD 发布按钮跳转流水线页面 (namespace+deployment 上下文) - 前端: api.ts 增加 pauseDeployment/resumeDeployment 方法 - 验证: 5项端到端测试全部通过 (Python urllib)
This commit is contained in:
1 parent
23f237288d
commit
2e66908e20
8 files changed
+194
-8
No files matched your search
@@ -46,8 +46,8 @@
|
|||||||
| ConfigMap/Secret 编辑 | ✅ | P2 | ✅ 已完成 M4 |
|
| ConfigMap/Secret 编辑 | ✅ | P2 | ✅ 已完成 M4 |
|
||||||
| **Deployment 可视化编辑器** | **P1** | **M6-M9 进行中 — 当前编辑器只有 image/resources/probe(httpGet)/env/mounts,缺容器完整编辑/存储/调度/服务** |
|
| **Deployment 可视化编辑器** | **P1** | **M6-M9 进行中 — 当前编辑器只有 image/resources/probe(httpGet)/env/mounts,缺容器完整编辑/存储/调度/服务** |
|
||||||
| **YAML 编辑器增强** | **P1** | **M6 — 当前是纯 textarea,需 Monaco/CodeMirror 语法高亮** |
|
| **YAML 编辑器增强** | **P1** | **M6 — 当前是纯 textarea,需 Monaco/CodeMirror 语法高亮** |
|
||||||
| CI/CD 集成菜单项 | P3 | 菜单有按钮但未接线 |
|
| CI/CD 集成菜单项 | ✅ | P3 | ✅ 已完成 M5 — 跳转流水线页面 |
|
||||||
| Deployment 暂停更新 | P3 | 菜单无此项 |
|
| Deployment 暂停更新 | ✅ | P3 | ✅ 已完成 M5 — pause/resume 端点 + 暂停 Badge |
|
||||||
|
|
||||||
### 📋 Deployment 编辑器现状评估 (2026-06-23)
|
### 📋 Deployment 编辑器现状评估 (2026-06-23)
|
||||||
|
|
||||||
|
|||||||
+8
-5
@@ -70,16 +70,19 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### M5: Deployment 暂停更新 + CI/CD 集成 [pending]
|
### M5: Deployment 暂停更新 + CI/CD 集成 [done]
|
||||||
**目标**: 接通顶部菜单的暂停更新和 CI/CD 按钮
|
**目标**: 接通顶部菜单的暂停更新和 CI/CD 按钮
|
||||||
|
|
||||||
**改动**:
|
**改动**:
|
||||||
- 后端 `POST .../deployments/:name/pause` 和 `/resume`
|
- 后端 `POST .../deployments/:name/pause` 和 `/resume`(ID 路由 + Kuboard code 路由)
|
||||||
- CI/CD 按钮跳转到已有 Pipeline 页面(传递 namespace+deployment 上下文)
|
- 客户端 `PauseDeployment()` / `ResumeDeployment()` — PATCH `spec.paused` 布尔字段
|
||||||
|
- Deployment 列表 summary 增加 `paused` 字段,前端可读取暂停状态
|
||||||
|
- 前端操作菜单增加「暂停更新/恢复更新」动态切换 + 暂停 Badge
|
||||||
|
- CI/CD 按钮跳转到流水线页面(传递 namespace+deployment 上下文)
|
||||||
|
|
||||||
**验收标准**:
|
**验收标准**:
|
||||||
- [ ] 可暂停/恢复 Deployment 滚动更新
|
- [x] 可暂停/恢复 Deployment 滚动更新
|
||||||
- [ ] CI/CD 按钮可跳转到流水线页面
|
- [x] CI/CD 按钮可跳转到流水线页面
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+42
@@ -94,3 +94,45 @@ curl -X POST "http://127.0.0.1:8080/api/v1/kubernetes/1/namespace/devops/pods/ai
|
|||||||
- [x] 文件列表 (ls) 能看到上传的文件
|
- [x] 文件列表 (ls) 能看到上传的文件
|
||||||
- [x] ExecInPod 使用 WebSocket channel.k8s.io 协议(不再用 SPDY raw HTTP)
|
- [x] ExecInPod 使用 WebSocket channel.k8s.io 协议(不再用 SPDY raw HTTP)
|
||||||
- [x] BusyBox ls 解析兼容(Mon DD HH:MM 格式)
|
- [x] BusyBox ls 解析兼容(Mon DD HH:MM 格式)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## M5: Deployment 暂停更新 + CI/CD 集成
|
||||||
|
|
||||||
|
### 验证用例
|
||||||
|
```bash
|
||||||
|
# U1: 暂停前 — 确认 paused=false
|
||||||
|
curl -s "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/namespace/devops/deployments" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" | jq '.data[] | select(.name=="redis") | .paused'
|
||||||
|
# 期望: false
|
||||||
|
|
||||||
|
# U2: 暂停 Deployment
|
||||||
|
curl -X POST "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/namespace/devops/deployments/redis/pause" \
|
||||||
|
-H "Authorization: Bearer $TOKEN"
|
||||||
|
# 期望: code=200, msg="已暂停滚动更新"
|
||||||
|
|
||||||
|
# U3: 暂停后 — 确认 paused=true
|
||||||
|
curl -s "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/namespace/devops/deployments" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" | jq '.data[] | select(.name=="redis") | .paused'
|
||||||
|
# 期望: true
|
||||||
|
|
||||||
|
# U4: 恢复 Deployment
|
||||||
|
curl -X POST "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/namespace/devops/deployments/redis/resume" \
|
||||||
|
-H "Authorization: Bearer $TOKEN"
|
||||||
|
# 期望: code=200, msg="已恢复滚动更新"
|
||||||
|
|
||||||
|
# U5: 恢复后 — 确认 paused=false
|
||||||
|
curl -s "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/namespace/devops/deployments" \
|
||||||
|
-H "Authorization: Bearer $TOKEN" | jq '.data[] | select(.name=="redis") | .paused'
|
||||||
|
# 期望: false
|
||||||
|
```
|
||||||
|
|
||||||
|
### 通过标准
|
||||||
|
- [x] U1: 暂停前 paused=false
|
||||||
|
- [x] U2: pause API 返回 200
|
||||||
|
- [x] U3: 暂停后 paused=true
|
||||||
|
- [x] U4: resume API 返回 200
|
||||||
|
- [x] U5: 恢复后 paused=false
|
||||||
|
- [x] ID 路由 + Kuboard code 路由均可用
|
||||||
|
- [x] 前端操作菜单显示「暂停更新/恢复更新」动态切换
|
||||||
|
- [x] CI/CD 发布按钮可跳转到流水线页面
|
||||||
@@ -1163,6 +1163,46 @@ func (h *KubernetesHandler) RestartDeployment(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "重启指令已下发"})
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "重启指令已下发"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PauseDeployment 暂停 Deployment 滚动更新(spec.paused=true)
|
||||||
|
func (h *KubernetesHandler) PauseDeployment(c *gin.Context) {
|
||||||
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
|
namespace := c.Param("namespace")
|
||||||
|
name := c.Param("name")
|
||||||
|
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.PauseDeployment(namespace, name); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "暂停更新失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已暂停滚动更新"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResumeDeployment 恢复 Deployment 滚动更新(spec.paused=false)
|
||||||
|
func (h *KubernetesHandler) ResumeDeployment(c *gin.Context) {
|
||||||
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
|
namespace := c.Param("namespace")
|
||||||
|
name := c.Param("name")
|
||||||
|
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.ResumeDeployment(namespace, name); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "恢复更新失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已恢复滚动更新"})
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// HPA 管理
|
// HPA 管理
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -1543,6 +1583,12 @@ func (h *KubernetesHandler) RestartDeploymentNS(c *gin.Context) {
|
|||||||
func (h *KubernetesHandler) UpdateDeploymentImageNS(c *gin.Context) {
|
func (h *KubernetesHandler) UpdateDeploymentImageNS(c *gin.Context) {
|
||||||
h.updateDeploymentImageWithNS(c, c.Param("namespace"))
|
h.updateDeploymentImageWithNS(c, c.Param("namespace"))
|
||||||
}
|
}
|
||||||
|
func (h *KubernetesHandler) PauseDeploymentNS(c *gin.Context) {
|
||||||
|
h.pauseDeploymentWithNS(c, c.Param("namespace"))
|
||||||
|
}
|
||||||
|
func (h *KubernetesHandler) ResumeDeploymentNS(c *gin.Context) {
|
||||||
|
h.resumeDeploymentWithNS(c, c.Param("namespace"))
|
||||||
|
}
|
||||||
func (h *KubernetesHandler) GetPodLogsNS(c *gin.Context) {
|
func (h *KubernetesHandler) GetPodLogsNS(c *gin.Context) {
|
||||||
h.getPodLogsWithNS(c, c.Param("namespace"))
|
h.getPodLogsWithNS(c, c.Param("namespace"))
|
||||||
}
|
}
|
||||||
@@ -1565,6 +1611,7 @@ func (h *KubernetesHandler) listDeploymentsWithNS(c *gin.Context, ns string) {
|
|||||||
AvailableReplicas int `json:"available_replicas"`
|
AvailableReplicas int `json:"available_replicas"`
|
||||||
ReadyReplicas int `json:"ready_replicas"`
|
ReadyReplicas int `json:"ready_replicas"`
|
||||||
Image string `json:"image"`
|
Image string `json:"image"`
|
||||||
|
Paused bool `json:"paused"`
|
||||||
}
|
}
|
||||||
result := []DepSummary{}
|
result := []DepSummary{}
|
||||||
for _, d := range deps.Items {
|
for _, d := range deps.Items {
|
||||||
@@ -1577,6 +1624,7 @@ func (h *KubernetesHandler) listDeploymentsWithNS(c *gin.Context, ns string) {
|
|||||||
AvailableReplicas: d.Status.AvailableReplicas,
|
AvailableReplicas: d.Status.AvailableReplicas,
|
||||||
ReadyReplicas: d.Status.ReadyReplicas,
|
ReadyReplicas: d.Status.ReadyReplicas,
|
||||||
Image: image,
|
Image: image,
|
||||||
|
Paused: d.Spec.Paused,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
c.JSON(200, models.Response{Code: 200, Data: result})
|
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||||
@@ -1816,6 +1864,22 @@ func (h *KubernetesHandler) updateDeploymentImageWithNS(c *gin.Context, ns strin
|
|||||||
if err := client.UpdateDeploymentImage(ns, c.Param("name"), req.ContainerName, req.Image); err != nil { c.JSON(500, models.Response{Code: 500, Message: "更新镜像失败: " + err.Error()}); return }
|
if err := client.UpdateDeploymentImage(ns, c.Param("name"), req.ContainerName, req.Image); err != nil { c.JSON(500, models.Response{Code: 500, Message: "更新镜像失败: " + err.Error()}); return }
|
||||||
c.JSON(200, models.Response{Code: 200, Message: fmt.Sprintf("镜像已更新为 %s", req.Image)})
|
c.JSON(200, models.Response{Code: 200, Message: fmt.Sprintf("镜像已更新为 %s", req.Image)})
|
||||||
}
|
}
|
||||||
|
func (h *KubernetesHandler) pauseDeploymentWithNS(c *gin.Context, ns string) {
|
||||||
|
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 }
|
||||||
|
if err := client.PauseDeployment(ns, c.Param("name")); err != nil { c.JSON(500, models.Response{Code: 500, Message: "暂停更新失败: " + err.Error()}); return }
|
||||||
|
c.JSON(200, models.Response{Code: 200, Message: "已暂停滚动更新"})
|
||||||
|
}
|
||||||
|
func (h *KubernetesHandler) resumeDeploymentWithNS(c *gin.Context, ns string) {
|
||||||
|
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 }
|
||||||
|
if err := client.ResumeDeployment(ns, c.Param("name")); err != nil { c.JSON(500, models.Response{Code: 500, Message: "恢复更新失败: " + err.Error()}); return }
|
||||||
|
c.JSON(200, models.Response{Code: 200, Message: "已恢复滚动更新"})
|
||||||
|
}
|
||||||
func (h *KubernetesHandler) getPodLogsWithNS(c *gin.Context, ns string) {
|
func (h *KubernetesHandler) getPodLogsWithNS(c *gin.Context, ns string) {
|
||||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||||
|
|||||||
@@ -298,6 +298,9 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
|||||||
// Deployment 扩缩容 & 重启 // Logan.Li
|
// Deployment 扩缩容 & 重启 // Logan.Li
|
||||||
clusters.POST("/:id/namespaces/:namespace/deployments/:name/scale", k8sHandler.ScaleDeployment)
|
clusters.POST("/:id/namespaces/:namespace/deployments/:name/scale", k8sHandler.ScaleDeployment)
|
||||||
clusters.POST("/:id/namespaces/:namespace/deployments/:name/restart", k8sHandler.RestartDeployment)
|
clusters.POST("/:id/namespaces/:namespace/deployments/:name/restart", k8sHandler.RestartDeployment)
|
||||||
|
// Deployment 暂停/恢复滚动更新 // Logan.Li M5
|
||||||
|
clusters.POST("/:id/namespaces/:namespace/deployments/:name/pause", k8sHandler.PauseDeployment)
|
||||||
|
clusters.POST("/:id/namespaces/:namespace/deployments/:name/resume", k8sHandler.ResumeDeployment)
|
||||||
// Deployment CI 集成 (Webhook 更新镜像) // Logan.Li
|
// Deployment CI 集成 (Webhook 更新镜像) // Logan.Li
|
||||||
clusters.POST("/:id/namespaces/:namespace/deployments/:name/update-image", k8sHandler.UpdateDeploymentImage)
|
clusters.POST("/:id/namespaces/:namespace/deployments/:name/update-image", k8sHandler.UpdateDeploymentImage)
|
||||||
// HPA 管理 // Logan.Li
|
// HPA 管理 // Logan.Li
|
||||||
@@ -337,6 +340,8 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
|||||||
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/deployments", k8sHandler.ListDeploymentsNS)
|
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/deployments", k8sHandler.ListDeploymentsNS)
|
||||||
kubernetesRouter.POST("/:clusterCode/namespace/:namespace/deployments/:name/scale", k8sHandler.ScaleDeploymentNS)
|
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/restart", k8sHandler.RestartDeploymentNS)
|
||||||
|
kubernetesRouter.POST("/:clusterCode/namespace/:namespace/deployments/:name/pause", k8sHandler.PauseDeploymentNS)
|
||||||
|
kubernetesRouter.POST("/:clusterCode/namespace/:namespace/deployments/:name/resume", k8sHandler.ResumeDeploymentNS)
|
||||||
kubernetesRouter.POST("/:clusterCode/namespace/:namespace/deployments/:name/update-image", k8sHandler.UpdateDeploymentImageNS)
|
kubernetesRouter.POST("/:clusterCode/namespace/:namespace/deployments/:name/update-image", k8sHandler.UpdateDeploymentImageNS)
|
||||||
|
|
||||||
// Pods
|
// Pods
|
||||||
|
|||||||
@@ -291,6 +291,7 @@ type DeploymentItem struct {
|
|||||||
type DeploymentSpec struct {
|
type DeploymentSpec struct {
|
||||||
Replicas int `json:"replicas"`
|
Replicas int `json:"replicas"`
|
||||||
Template DeploymentTemplate `json:"template"`
|
Template DeploymentTemplate `json:"template"`
|
||||||
|
Paused bool `json:"paused"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeploymentTemplate struct {
|
type DeploymentTemplate struct {
|
||||||
@@ -586,6 +587,26 @@ func (c *Client) RestartDeployment(namespace, name string) error {
|
|||||||
return c.patch(path, body, nil)
|
return c.patch(path, body, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PauseDeployment pauses rollout of a Deployment by setting spec.paused=true.
|
||||||
|
// A paused Deployment will not process new rollouts — useful for debugging or
|
||||||
|
// staged updates. Resume with ResumeDeployment.
|
||||||
|
func (c *Client) PauseDeployment(namespace, name string) error {
|
||||||
|
body := map[string]interface{}{
|
||||||
|
"spec": map[string]interface{}{"paused": true},
|
||||||
|
}
|
||||||
|
path := fmt.Sprintf("/apis/apps/v1/namespaces/%s/deployments/%s", namespace, name)
|
||||||
|
return c.patch(path, body, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResumeDeployment resumes rollout of a paused Deployment by setting spec.paused=false.
|
||||||
|
func (c *Client) ResumeDeployment(namespace, name string) error {
|
||||||
|
body := map[string]interface{}{
|
||||||
|
"spec": map[string]interface{}{"paused": false},
|
||||||
|
}
|
||||||
|
path := fmt.Sprintf("/apis/apps/v1/namespaces/%s/deployments/%s", namespace, name)
|
||||||
|
return c.patch(path, body, nil)
|
||||||
|
}
|
||||||
|
|
||||||
// --- Exec (K8s WebSocket channel protocol) ---
|
// --- Exec (K8s WebSocket channel protocol) ---
|
||||||
|
|
||||||
// ExecResult holds the output of a pod exec command
|
// ExecResult holds the output of a pod exec command
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// DeploymentWorkload.tsx — Kuboard 风格 Deployment 详情主组件
|
// DeploymentWorkload.tsx — Kuboard 风格 Deployment 详情主组件
|
||||||
// - 顶部操作菜单: 编辑 | 预览/编辑YAML | 调整镜像版本 | 伸缩 | 日志/终端 | 重启 | 删除 | 元数据 | 运行时 | 服务 | 应用路由
|
// - 顶部操作菜单: 编辑 | 预览/编辑YAML | 调整镜像版本 | 伸缩 | 日志/终端 | 重启 | 暂停/恢复更新 | CI/CD发布 | 删除 | 元数据 | 运行时 | 服务 | 应用路由
|
||||||
// - 双模式: view(只读增强看板)/ edit(表单编辑器)
|
// - 双模式: view(只读增强看板)/ edit(表单编辑器)
|
||||||
// 作者: Logan <admin@attacker.club>
|
// 作者: Logan <admin@attacker.club>
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
Trash2, FileText, Terminal, RefreshCw, Loader2, X,
|
Trash2, FileText, Terminal, RefreshCw, Loader2, X,
|
||||||
ChevronDown, Cpu, Network, GitBranch, Info,
|
ChevronDown, Cpu, Network, GitBranch, Info,
|
||||||
CheckCircle, AlertTriangle, XCircle,
|
CheckCircle, AlertTriangle, XCircle,
|
||||||
|
Pause, Play, Rocket,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { kubernetesApi } from '@/services/api';
|
import { kubernetesApi } from '@/services/api';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
@@ -163,6 +164,29 @@ export function DeploymentWorkload({ clusterCode, namespace, deploymentName, mod
|
|||||||
} finally { setActionLoading(''); }
|
} finally { setActionLoading(''); }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTogglePause = async () => {
|
||||||
|
const isPaused = !!deployment?.paused;
|
||||||
|
const action = isPaused ? '恢复' : '暂停';
|
||||||
|
if (!confirm(`确认${action} Deployment "${deploymentName}" 的滚动更新?`)) return;
|
||||||
|
setActionLoading(isPaused ? 'resume' : 'pause');
|
||||||
|
try {
|
||||||
|
if (isPaused) {
|
||||||
|
await kubernetesApi.resumeDeployment(clusterCode, namespace, deploymentName);
|
||||||
|
} else {
|
||||||
|
await kubernetesApi.pauseDeployment(clusterCode, namespace, deploymentName);
|
||||||
|
}
|
||||||
|
toast({ title: `${action}成功`, description: `Deployment 滚动更新已${action}` });
|
||||||
|
setTimeout(loadData, 1000);
|
||||||
|
} catch (e: any) {
|
||||||
|
toast({ title: `${action}失败`, description: e?.message || String(e), variant: 'destructive' });
|
||||||
|
} finally { setActionLoading(''); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCICD = () => {
|
||||||
|
// 跳转到流水线页面,传递 namespace + deployment 上下文
|
||||||
|
navigate(`/pipelines?namespace=${encodeURIComponent(namespace)}&app=${encodeURIComponent(deploymentName)}`);
|
||||||
|
};
|
||||||
|
|
||||||
const handleUpdateImage = async () => {
|
const handleUpdateImage = async () => {
|
||||||
if (!newImage.trim()) return;
|
if (!newImage.trim()) return;
|
||||||
setActionLoading('image');
|
setActionLoading('image');
|
||||||
@@ -250,6 +274,11 @@ export function DeploymentWorkload({ clusterCode, namespace, deploymentName, mod
|
|||||||
{isHealthy ? <CheckCircle className="h-3 w-3 mr-1" /> : <AlertTriangle className="h-3 w-3 mr-1" />}
|
{isHealthy ? <CheckCircle className="h-3 w-3 mr-1" /> : <AlertTriangle className="h-3 w-3 mr-1" />}
|
||||||
{ready}/{total}
|
{ready}/{total}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{deployment?.paused && (
|
||||||
|
<Badge className="bg-orange-500/20 text-orange-400 border-orange-500/30">
|
||||||
|
<Pause className="h-3 w-3 mr-1" />已暂停更新
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-slate-500 mt-0.5 truncate">
|
<p className="text-xs text-slate-500 mt-0.5 truncate">
|
||||||
镜像: <code className="text-slate-400">{deployment.image || '-'}</code>
|
镜像: <code className="text-slate-400">{deployment.image || '-'}</code>
|
||||||
@@ -304,6 +333,22 @@ export function DeploymentWorkload({ clusterCode, namespace, deploymentName, mod
|
|||||||
<DropdownMenuItem className="text-slate-200 hover:bg-slate-800 cursor-pointer" onClick={handleRestart}>
|
<DropdownMenuItem className="text-slate-200 hover:bg-slate-800 cursor-pointer" onClick={handleRestart}>
|
||||||
<RotateCw className="h-4 w-4 mr-2 text-blue-400" /> 重启
|
<RotateCw className="h-4 w-4 mr-2 text-blue-400" /> 重启
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="text-slate-200 hover:bg-slate-800 cursor-pointer"
|
||||||
|
disabled={!!actionLoading}
|
||||||
|
onClick={handleTogglePause}
|
||||||
|
>
|
||||||
|
{deployment?.paused
|
||||||
|
? <><Play className="h-4 w-4 mr-2 text-green-400" /> 恢复更新</>
|
||||||
|
: <><Pause className="h-4 w-4 mr-2 text-orange-400" /> 暂停更新</>
|
||||||
|
}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="text-slate-200 hover:bg-slate-800 cursor-pointer"
|
||||||
|
onClick={handleCICD}
|
||||||
|
>
|
||||||
|
<Rocket className="h-4 w-4 mr-2 text-purple-400" /> CI/CD 发布
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem className="text-red-400 hover:bg-slate-800 cursor-pointer" onClick={() => setModal({ kind: 'delete' })}>
|
<DropdownMenuItem className="text-red-400 hover:bg-slate-800 cursor-pointer" onClick={() => setModal({ kind: 'delete' })}>
|
||||||
<Trash2 className="h-4 w-4 mr-2" /> 删除
|
<Trash2 className="h-4 w-4 mr-2" /> 删除
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
@@ -344,6 +344,12 @@ export const kubernetesApi = {
|
|||||||
restartDeployment: async (clusterCode: string, namespace: string, name: string) => {
|
restartDeployment: async (clusterCode: string, namespace: string, name: string) => {
|
||||||
return http.post(`/kubernetes/${clusterCode}/namespace/${namespace}/deployments/${name}/restart`);
|
return http.post(`/kubernetes/${clusterCode}/namespace/${namespace}/deployments/${name}/restart`);
|
||||||
},
|
},
|
||||||
|
pauseDeployment: async (clusterCode: string, namespace: string, name: string) => {
|
||||||
|
return http.post(`/kubernetes/${clusterCode}/namespace/${namespace}/deployments/${name}/pause`);
|
||||||
|
},
|
||||||
|
resumeDeployment: async (clusterCode: string, namespace: string, name: string) => {
|
||||||
|
return http.post(`/kubernetes/${clusterCode}/namespace/${namespace}/deployments/${name}/resume`);
|
||||||
|
},
|
||||||
updateDeploymentImage: async (clusterCode: string, namespace: string, name: string, image: string, containerName?: string) => {
|
updateDeploymentImage: async (clusterCode: string, namespace: string, name: string, image: string, containerName?: string) => {
|
||||||
return http.post(`/kubernetes/${clusterCode}/namespace/${namespace}/deployments/${name}/update-image`, { image, container_name: containerName });
|
return http.post(`/kubernetes/${clusterCode}/namespace/${namespace}/deployments/${name}/update-image`, { image, container_name: containerName });
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in new issue
Block a user