- 新增 log_datasource.go / log_proxy.go / user_api_token.go 后端控制器 - 新增 log_datasource.go model - 新增 monitor/log-center.ts API 模块 - 新增 LogCenter 前端组件及页面 - 重构 deployment 相关组件 (HPDA/UpdateImageModal/bundle-yaml 等) - 新增 ResetPasswordDialog / code-snippet-block / copy-button 等 UI 组件 - 重构多个 k8s 相关组件 (TopologyView/ResourceVisualEditor/PodFileBrowser 等) - 更新多个 API 和工具函数 (auth.ts/monitor/index.ts/request.ts 等) - 更新静态资源 (index.html + 新 JS/CSS bundle) - 删除废弃静态资源文件
699 lines
30 KiB
Go
699 lines
30 KiB
Go
package controller
|
||
|
||
import (
|
||
"fmt"
|
||
"net/http"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"ezops/model"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
func (h *KubernetesHandler) ListDeployments(c *gin.Context) {
|
||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||
namespace := c.Query("namespace")
|
||
|
||
client, _, err := h.getK8sClient(clusterID)
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||
return
|
||
}
|
||
deps, err := client.ListDeployments()
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
type DepSummary struct {
|
||
Namespace string `json:"namespace"`
|
||
Name string `json:"name"`
|
||
Replicas int `json:"replicas"`
|
||
AvailableReplicas int `json:"available_replicas"`
|
||
ReadyReplicas int `json:"ready_replicas"`
|
||
Image string `json:"image"`
|
||
}
|
||
result := []DepSummary{}
|
||
for _, d := range deps.Items {
|
||
if namespace != "" && d.Metadata.Namespace != namespace {
|
||
continue
|
||
}
|
||
image := ""
|
||
if len(d.Spec.Template.Spec.Containers) > 0 {
|
||
image = d.Spec.Template.Spec.Containers[0].Image
|
||
}
|
||
result = append(result, DepSummary{
|
||
Namespace: d.Metadata.Namespace,
|
||
Name: d.Metadata.Name,
|
||
Replicas: d.Spec.Replicas,
|
||
AvailableReplicas: d.Status.AvailableReplicas,
|
||
ReadyReplicas: d.Status.ReadyReplicas,
|
||
Image: image,
|
||
})
|
||
}
|
||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "success", Data: result})
|
||
}
|
||
|
||
// ListServices 获取Service列表
|
||
// @Summary 获取Service列表
|
||
// @Description 获取指定集群的Service列表
|
||
// @Tags K8s集群
|
||
// @Produce json
|
||
// @Security Bearer
|
||
// @Param id path int true "集群ID"
|
||
// @Param namespace query string false "命名空间"
|
||
// @Success 200 {object} model.Response{data=[]model.KubernetesService}
|
||
// @Router /kubernetes-clusters/{id}/services [get]
|
||
func (h *KubernetesHandler) ScaleDeployment(c *gin.Context) {
|
||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||
namespace := c.Param("namespace")
|
||
name := c.Param("name")
|
||
|
||
var req struct {
|
||
Replicas int `json:"replicas"`
|
||
}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误"})
|
||
return
|
||
}
|
||
if req.Replicas < 0 {
|
||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "副本数不能为负数"})
|
||
return
|
||
}
|
||
|
||
client, _, err := h.getK8sClient(clusterID)
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||
return
|
||
}
|
||
|
||
if err := client.ScaleDeployment(namespace, name, req.Replicas); err != nil {
|
||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "扩缩容失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: fmt.Sprintf("已设置为 %d 副本", req.Replicas)})
|
||
}
|
||
|
||
// RestartDeployment 重启 Deployment (滚动重启)
|
||
func (h *KubernetesHandler) RestartDeployment(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, model.Response{Code: 500, Message: err.Error()})
|
||
return
|
||
}
|
||
|
||
if err := client.RestartDeployment(namespace, name); err != nil {
|
||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "重启失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, model.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, model.Response{Code: 500, Message: err.Error()})
|
||
return
|
||
}
|
||
|
||
if err := client.PauseDeployment(namespace, name); err != nil {
|
||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "暂停更新失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, model.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, model.Response{Code: 500, Message: err.Error()})
|
||
return
|
||
}
|
||
|
||
if err := client.ResumeDeployment(namespace, name); err != nil {
|
||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "恢复更新失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "已恢复滚动更新"})
|
||
}
|
||
|
||
// ============================================================================
|
||
// HPA 管理
|
||
// ============================================================================
|
||
|
||
// ListHPAs 获取 HPA 列表
|
||
func (h *KubernetesHandler) UpdateDeploymentImage(c *gin.Context) {
|
||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||
namespace := c.Param("namespace")
|
||
name := c.Param("name")
|
||
|
||
var req struct {
|
||
Image string `json:"image"`
|
||
ContainerName string `json:"container_name"`
|
||
}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误"})
|
||
return
|
||
}
|
||
if req.Image == "" {
|
||
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "image 不能为空"})
|
||
return
|
||
}
|
||
|
||
client, _, err := h.getK8sClient(clusterID)
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
||
return
|
||
}
|
||
|
||
if err := client.UpdateDeploymentImage(namespace, name, req.ContainerName, req.Image); err != nil {
|
||
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "更新镜像失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, model.Response{Code: 200, Message: fmt.Sprintf("镜像已更新为 %s,正在滚动更新...", req.Image)})
|
||
}
|
||
|
||
// ============================================================================
|
||
// Namespace
|
||
// ============================================================================
|
||
|
||
// ListNamespaces 获取命名空间列表
|
||
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, model.Response{Code: 500, Message: err.Error()}); return }
|
||
deps, err := client.ListDeployments()
|
||
if err != nil { c.JSON(500, model.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, model.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, model.Response{Code: 404, Message: "Deployment not found"})
|
||
}
|
||
func (h *KubernetesHandler) ScaleDeploymentNS(c *gin.Context) {
|
||
h.scaleDeploymentWithNS(c, c.Param("namespace"))
|
||
}
|
||
func (h *KubernetesHandler) RestartDeploymentNS(c *gin.Context) {
|
||
h.restartDeploymentWithNS(c, c.Param("namespace"))
|
||
}
|
||
func (h *KubernetesHandler) UpdateDeploymentImageNS(c *gin.Context) {
|
||
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) listDeploymentsWithNS(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, model.Response{Code: 500, Message: err.Error()}); return }
|
||
deps, err := client.ListDeployments()
|
||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||
type DepSummary struct {
|
||
Namespace string `json:"namespace"`
|
||
Name string `json:"name"`
|
||
Replicas int `json:"replicas"`
|
||
AvailableReplicas int `json:"available_replicas"`
|
||
ReadyReplicas int `json:"ready_replicas"`
|
||
Image string `json:"image"`
|
||
Paused bool `json:"paused"`
|
||
Selector map[string]string `json:"selector,omitempty"` // matchLabels — 前端 Service Tab 关联用
|
||
}
|
||
result := []DepSummary{}
|
||
for _, d := range deps.Items {
|
||
if d.Metadata.Namespace != ns { continue }
|
||
image := ""; if len(d.Spec.Template.Spec.Containers) > 0 { image = d.Spec.Template.Spec.Containers[0].Image }
|
||
result = append(result, DepSummary{
|
||
Namespace: d.Metadata.Namespace,
|
||
Name: d.Metadata.Name,
|
||
Replicas: d.Spec.Replicas,
|
||
AvailableReplicas: d.Status.AvailableReplicas,
|
||
ReadyReplicas: d.Status.ReadyReplicas,
|
||
Image: image,
|
||
Paused: d.Spec.Paused,
|
||
Selector: func() map[string]string { if d.Spec.Selector != nil { return d.Spec.Selector.MatchLabels }; return nil }(),
|
||
})
|
||
}
|
||
c.JSON(200, model.Response{Code: 200, Data: result})
|
||
}
|
||
func (h *KubernetesHandler) scaleDeploymentWithNS(c *gin.Context, ns string) {
|
||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||
var req struct { Replicas int `json:"replicas"` }
|
||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误"}); return }
|
||
client, _, err := h.getK8sClient(clusterID)
|
||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||
if err := client.ScaleDeployment(ns, c.Param("name"), req.Replicas); err != nil { c.JSON(500, model.Response{Code: 500, Message: "扩缩容失败: " + err.Error()}); return }
|
||
c.JSON(200, model.Response{Code: 200, Message: fmt.Sprintf("已设置为 %d 副本", req.Replicas)})
|
||
}
|
||
func (h *KubernetesHandler) restartDeploymentWithNS(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, model.Response{Code: 500, Message: err.Error()}); return }
|
||
if err := client.RestartDeployment(ns, c.Param("name")); err != nil { c.JSON(500, model.Response{Code: 500, Message: "重启失败: " + err.Error()}); return }
|
||
c.JSON(200, model.Response{Code: 200, Message: "重启指令已下发"})
|
||
}
|
||
func (h *KubernetesHandler) updateDeploymentImageWithNS(c *gin.Context, ns string) {
|
||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||
var req struct { Image string `json:"image"`; ContainerName string `json:"container_name"` }
|
||
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, model.Response{Code: 400, Message: "参数错误"}); return }
|
||
if strings.TrimSpace(req.Image) == "" { c.JSON(400, model.Response{Code: 400, Message: "image 不能为空"}); return }
|
||
client, _, err := h.getK8sClient(clusterID)
|
||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||
if err := client.UpdateDeploymentImage(ns, c.Param("name"), req.ContainerName, req.Image); err != nil { c.JSON(500, model.Response{Code: 500, Message: "更新镜像失败: " + err.Error()}); return }
|
||
c.JSON(200, model.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, model.Response{Code: 500, Message: err.Error()}); return }
|
||
if err := client.PauseDeployment(ns, c.Param("name")); err != nil { c.JSON(500, model.Response{Code: 500, Message: "暂停更新失败: " + err.Error()}); return }
|
||
c.JSON(200, model.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, model.Response{Code: 500, Message: err.Error()}); return }
|
||
if err := client.ResumeDeployment(ns, c.Param("name")); err != nil { c.JSON(500, model.Response{Code: 500, Message: "恢复更新失败: " + err.Error()}); return }
|
||
c.JSON(200, model.Response{Code: 200, Message: "已恢复滚动更新"})
|
||
}
|
||
func (h *KubernetesHandler) ListDeploymentsAllNS(c *gin.Context) {
|
||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||
client, _, err := h.getK8sClient(clusterID)
|
||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||
deps, err := client.ListDeployments()
|
||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||
type DepSummary struct {
|
||
Namespace string `json:"namespace"`
|
||
Name string `json:"name"`
|
||
Replicas int `json:"replicas"`
|
||
AvailableReplicas int `json:"available_replicas"`
|
||
ReadyReplicas int `json:"ready_replicas"`
|
||
Image string `json:"image"`
|
||
}
|
||
result := []DepSummary{}
|
||
for _, d := range deps.Items {
|
||
image := ""; if len(d.Spec.Template.Spec.Containers) > 0 { image = d.Spec.Template.Spec.Containers[0].Image }
|
||
result = append(result, DepSummary{
|
||
Namespace: d.Metadata.Namespace,
|
||
Name: d.Metadata.Name,
|
||
Replicas: d.Spec.Replicas,
|
||
AvailableReplicas: d.Status.AvailableReplicas,
|
||
ReadyReplicas: d.Status.ReadyReplicas,
|
||
Image: image,
|
||
})
|
||
}
|
||
c.JSON(200, model.Response{Code: 200, Data: result})
|
||
}
|
||
|
||
// ListPodsAllNS lists all pods across all namespaces using clusterCode.
|
||
func (h *KubernetesHandler) ListStatefulSetsNS(c *gin.Context) {
|
||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||
client, _, err := h.getK8sClient(clusterID)
|
||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||
sts, err := client.ListStatefulSets()
|
||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||
ns := c.Param("namespace")
|
||
type StsSummary struct {
|
||
Namespace string `json:"namespace"`
|
||
Name string `json:"name"`
|
||
Replicas int `json:"replicas"`
|
||
Ready int `json:"ready"`
|
||
Image string `json:"image"`
|
||
}
|
||
result := []StsSummary{}
|
||
for _, s := range sts.Items {
|
||
if s.Metadata.Namespace != ns { continue }
|
||
image := ""; if len(s.Spec.Template.Spec.Containers) > 0 { image = s.Spec.Template.Spec.Containers[0].Image }
|
||
result = append(result, StsSummary{
|
||
Namespace: s.Metadata.Namespace,
|
||
Name: s.Metadata.Name,
|
||
Replicas: s.Spec.Replicas,
|
||
Ready: s.Status.ReadyReplicas,
|
||
Image: image,
|
||
})
|
||
}
|
||
c.JSON(200, model.Response{Code: 200, Data: result})
|
||
}
|
||
|
||
// ListDaemonSetsNS lists DaemonSets in a namespace
|
||
func (h *KubernetesHandler) ListDaemonSetsNS(c *gin.Context) {
|
||
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||
if clusterID == 0 { c.JSON(500, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||
client, _, err := h.getK8sClient(clusterID)
|
||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||
ds, err := client.ListDaemonSets()
|
||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||
ns := c.Param("namespace")
|
||
type DsSummary struct {
|
||
Namespace string `json:"namespace"`
|
||
Name string `json:"name"`
|
||
Desired int `json:"desired"`
|
||
Current int `json:"current"`
|
||
Ready int `json:"ready"`
|
||
Image string `json:"image"`
|
||
}
|
||
result := []DsSummary{}
|
||
for _, d := range ds.Items {
|
||
if d.Metadata.Namespace != ns { continue }
|
||
image := ""; if len(d.Spec.Template.Spec.Containers) > 0 { image = d.Spec.Template.Spec.Containers[0].Image }
|
||
result = append(result, DsSummary{
|
||
Namespace: d.Metadata.Namespace,
|
||
Name: d.Metadata.Name,
|
||
Desired: d.Status.DesiredNumberScheduled,
|
||
Current: d.Status.CurrentNumberScheduled,
|
||
Ready: d.Status.NumberReady,
|
||
Image: image,
|
||
})
|
||
}
|
||
c.JSON(200, model.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, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||
client, _, err := h.getK8sClient(clusterID)
|
||
if err != nil { c.JSON(500, model.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, model.Response{Code: 400, Message: "参数错误"}); return }
|
||
if err := client.ScaleStatefulSet(ns, name, req.Replicas); err != nil {
|
||
c.JSON(500, model.Response{Code: 500, Message: "扩缩容失败: " + err.Error()}); return
|
||
}
|
||
c.JSON(200, model.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, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||
client, _, err := h.getK8sClient(clusterID)
|
||
if err != nil { c.JSON(500, model.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, model.Response{Code: 400, Message: "参数错误"}); return }
|
||
if err := client.ScaleStatefulSet(ns, name, req.Replicas); err != nil {
|
||
c.JSON(500, model.Response{Code: 500, Message: "扩缩容失败: " + err.Error()}); return
|
||
}
|
||
c.JSON(200, model.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, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||
client, _, err := h.getK8sClient(clusterID)
|
||
if err != nil { c.JSON(500, model.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, model.Response{Code: 500, Message: "重启失败: " + err.Error()}); return
|
||
}
|
||
c.JSON(200, model.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, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||
client, _, err := h.getK8sClient(clusterID)
|
||
if err != nil { c.JSON(500, model.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, model.Response{Code: 500, Message: "重启失败: " + err.Error()}); return
|
||
}
|
||
c.JSON(200, model.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, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||
client, _, err := h.getK8sClient(clusterID)
|
||
if err != nil { c.JSON(500, model.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, model.Response{Code: 500, Message: "重启失败: " + err.Error()}); return
|
||
}
|
||
c.JSON(200, model.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, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||
client, _, err := h.getK8sClient(clusterID)
|
||
if err != nil { c.JSON(500, model.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, model.Response{Code: 500, Message: "重启失败: " + err.Error()}); return
|
||
}
|
||
c.JSON(200, model.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, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||
client, _, err := h.getK8sClient(clusterID)
|
||
if err != nil { c.JSON(500, model.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, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||
c.JSON(200, model.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, model.Response{Code: 500, Message: "cluster not found"}); return }
|
||
client, _, err := h.getK8sClient(clusterID)
|
||
if err != nil { c.JSON(500, model.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, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||
c.JSON(200, model.Response{Code: 200, Data: ds})
|
||
}
|
||
|
||
// ============================================================================
|
||
// Pod Metrics (kubectl top pods) — per-container CPU/Memory usage via metrics-server
|
||
// ============================================================================
|
||
|
||
// GetPodMetrics returns pod-level CPU/Memory metrics
|
||
func (h *KubernetesHandler) ListReplicaSetsNS(c *gin.Context) {
|
||
h.listReplicaSetsWithNS(c, c.Param("namespace"))
|
||
}
|
||
func (h *KubernetesHandler) listReplicaSetsWithNS(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, model.Response{Code: 500, Message: err.Error()}); return }
|
||
items, err := client.ListReplicaSetsRaw(ns)
|
||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||
type RSSummary struct {
|
||
Namespace string `json:"namespace"`
|
||
Name string `json:"name"`
|
||
Replicas int `json:"replicas"`
|
||
AvailableReplicas int `json:"available_replicas"`
|
||
ReadyReplicas int `json:"ready_replicas"`
|
||
Image string `json:"image"`
|
||
CreatedAt string `json:"created_at"`
|
||
Revision string `json:"revision"`
|
||
OwnerDeployment string `json:"owner_deployment"`
|
||
}
|
||
result := []RSSummary{}
|
||
for _, rs := range items {
|
||
image := ""
|
||
if len(rs.Spec.Template.Spec.Containers) > 0 { image = rs.Spec.Template.Spec.Containers[0].Image }
|
||
revision := rs.Metadata.Annotations["deployment.kubernetes.io/revision"]
|
||
owner := ""
|
||
for _, o := range rs.Metadata.OwnerReferences {
|
||
if o.Kind == "Deployment" { owner = o.Name; break }
|
||
}
|
||
result = append(result, RSSummary{
|
||
Namespace: rs.Metadata.Namespace,
|
||
Name: rs.Metadata.Name,
|
||
Replicas: rs.Spec.Replicas,
|
||
AvailableReplicas: rs.Status.AvailableReplicas,
|
||
ReadyReplicas: rs.Status.ReadyReplicas,
|
||
Image: image,
|
||
CreatedAt: rs.Metadata.CreationTimestamp,
|
||
Revision: revision,
|
||
OwnerDeployment: owner,
|
||
})
|
||
}
|
||
c.JSON(200, model.Response{Code: 200, Message: "success", Data: result})
|
||
}
|
||
|
||
// ListDeploymentRevisionsNS Deployment 发布历史(按 revision 倒序)
|
||
func (h *KubernetesHandler) ListDeploymentRevisionsNS(c *gin.Context) {
|
||
h.listDeploymentRevisionsWithNS(c, c.Param("namespace"))
|
||
}
|
||
func (h *KubernetesHandler) listDeploymentRevisionsWithNS(c *gin.Context, ns string) {
|
||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||
deploymentName := c.Param("name")
|
||
if deploymentName == "" { c.JSON(400, model.Response{Code: 400, Message: "缺少 deployment name"}); return }
|
||
client, _, err := h.getK8sClient(clusterID)
|
||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||
items, err := client.ListReplicaSetsRaw(ns)
|
||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||
type Revision struct {
|
||
Revision string `json:"revision"`
|
||
ReplicaSet string `json:"replica_set"`
|
||
Replicas int `json:"replicas"`
|
||
Image string `json:"image"`
|
||
CreatedAt string `json:"created_at"`
|
||
Current bool `json:"current"`
|
||
}
|
||
result := []Revision{}
|
||
for _, rs := range items {
|
||
isOwner := false
|
||
for _, o := range rs.Metadata.OwnerReferences {
|
||
if o.Kind == "Deployment" && o.Name == deploymentName { isOwner = true; break }
|
||
}
|
||
if !isOwner { continue }
|
||
image := ""
|
||
if len(rs.Spec.Template.Spec.Containers) > 0 { image = rs.Spec.Template.Spec.Containers[0].Image }
|
||
result = append(result, Revision{
|
||
Revision: rs.Metadata.Annotations["deployment.kubernetes.io/revision"],
|
||
ReplicaSet: rs.Metadata.Name,
|
||
Replicas: rs.Spec.Replicas,
|
||
Image: image,
|
||
CreatedAt: rs.Metadata.CreationTimestamp,
|
||
Current: rs.Spec.Replicas > 0,
|
||
})
|
||
}
|
||
// 按 revision 数字倒序(最新在前)
|
||
sort.Slice(result, func(i, j int) bool {
|
||
ri, _ := strconv.Atoi(result[i].Revision)
|
||
rj, _ := strconv.Atoi(result[j].Revision)
|
||
return ri > rj
|
||
})
|
||
c.JSON(200, model.Response{Code: 200, Message: "success", Data: result})
|
||
}
|
||
|
||
// RollbackDeploymentNS 回滚 Deployment 到指定 revision 或指定 ReplicaSet
|
||
func (h *KubernetesHandler) RollbackDeploymentNS(c *gin.Context) {
|
||
h.rollbackDeploymentWithNS(c, c.Param("namespace"))
|
||
}
|
||
func (h *KubernetesHandler) rollbackDeploymentWithNS(c *gin.Context, ns string) {
|
||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||
deploymentName := c.Param("name")
|
||
if deploymentName == "" { c.JSON(400, model.Response{Code: 400, Message: "缺少 deployment name"}); return }
|
||
var req struct {
|
||
Revision int `json:"revision"`
|
||
ReplicaSet string `json:"replicaSet"`
|
||
}
|
||
_ = c.ShouldBindJSON(&req) // 允许空 body(但下面会校验)
|
||
client, _, err := h.getK8sClient(clusterID)
|
||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: err.Error()}); return }
|
||
|
||
targetRS := req.ReplicaSet
|
||
// 通过 revision 定位 ReplicaSet
|
||
if targetRS == "" && req.Revision > 0 {
|
||
items, err := client.ListReplicaSetsRaw(ns)
|
||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "获取 ReplicaSet 列表失败: " + err.Error()}); return }
|
||
for _, rs := range items {
|
||
isOwner := false
|
||
for _, o := range rs.Metadata.OwnerReferences {
|
||
if o.Kind == "Deployment" && o.Name == deploymentName { isOwner = true; break }
|
||
}
|
||
if !isOwner { continue }
|
||
if rs.Metadata.Annotations["deployment.kubernetes.io/revision"] == strconv.Itoa(req.Revision) {
|
||
targetRS = rs.Metadata.Name
|
||
break
|
||
}
|
||
}
|
||
if targetRS == "" {
|
||
c.JSON(404, model.Response{Code: 404, Message: fmt.Sprintf("revision %d 的 ReplicaSet 未找到", req.Revision)})
|
||
return
|
||
}
|
||
}
|
||
if targetRS == "" { c.JSON(400, model.Response{Code: 400, Message: "需要 revision 或 replicaSet 参数"}); return }
|
||
|
||
// 取 ReplicaSet 的 template,patch 到 Deployment.spec.template 实现回滚
|
||
rsJSON, err := client.GetReplicaSet(ns, targetRS)
|
||
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "获取 ReplicaSet 失败: " + err.Error()}); return }
|
||
specMap, ok := rsJSON["spec"].(map[string]interface{})
|
||
if !ok { c.JSON(500, model.Response{Code: 500, Message: "ReplicaSet 缺少 spec"}); return }
|
||
template, ok := specMap["template"].(map[string]interface{})
|
||
if !ok { c.JSON(500, model.Response{Code: 500, Message: "ReplicaSet 缺少 template"}); return }
|
||
|
||
if err := client.PatchDeploymentSpecTemplate(ns, deploymentName, template); err != nil {
|
||
c.JSON(500, model.Response{Code: 500, Message: "回滚失败: " + err.Error()}); return
|
||
}
|
||
c.JSON(200, model.Response{
|
||
Code: 200,
|
||
Message: fmt.Sprintf("Deployment %s 已回滚到 %s", deploymentName, targetRS),
|
||
Data: gin.H{"replicaSet": targetRS, "revision": req.Revision},
|
||
})
|
||
}
|
||
|
||
// ============================================================================
|
||
// Pod 删除 // Logan.Li
|
||
// ============================================================================
|
||
|
||
// DeletePodNS 删除指定 Pod
|