后端: - 新增 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=✅
503 lines
16 KiB
Go
503 lines
16 KiB
Go
package controller
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"ezops/model"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func (h *KubernetesHandler) ListServices(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
|
|
}
|
|
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})
|
|
}
|
|
|
|
// ListIngresses 获取Ingress列表
|
|
// @Summary 获取Ingress列表
|
|
// @Description 获取指定集群的Ingress列表
|
|
// @Tags K8s集群
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param id path int true "集群ID"
|
|
// @Param namespace query string false "命名空间"
|
|
// @Success 200 {object} model.Response{data=[]model.KubernetesIngress}
|
|
// @Router /kubernetes-clusters/{id}/ingresses [get]
|
|
func (h *KubernetesHandler) ListIngresses(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
|
|
}
|
|
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})
|
|
}
|
|
|
|
// 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")
|
|
var layout model.TopologyLayout
|
|
result := h.db.Where("cluster_id = ?", clusterID).First(&layout)
|
|
if result.Error != nil {
|
|
c.JSON(http.StatusOK, model.Response{Code: 200, Data: &model.TopologyLayout{}})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, model.Response{Code: 200, Data: layout})
|
|
}
|
|
|
|
// SaveTopology 保存集群拓扑布局 // Logan.Li
|
|
func (h *KubernetesHandler) SaveTopology(c *gin.Context) {
|
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
|
var req struct {
|
|
LayoutData string `json:"layout_data"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "参数错误"})
|
|
return
|
|
}
|
|
|
|
var layout model.TopologyLayout
|
|
h.db.Where("cluster_id = ?", clusterID).First(&layout)
|
|
layout.ClusterID = uint(clusterID)
|
|
layout.LayoutData = req.LayoutData
|
|
if layout.ID == 0 {
|
|
h.db.Create(&layout)
|
|
} else {
|
|
h.db.Save(&layout)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "保存成功"})
|
|
}
|
|
|
|
|
|
// SyncCluster 同步集群资源(连接真实K8s API拉取数据)
|
|
func (h *KubernetesHandler) ListConfigMaps(c *gin.Context) {
|
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
|
if clusterID == 0 {
|
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
|
}
|
|
namespace := c.Param("namespace")
|
|
|
|
client, _, err := h.getK8sClient(clusterID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
cms, err := client.ListConfigMaps(namespace)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
// 扁平化字段,与前端表格列 name/namespace/key_count 对齐 // Logan.Li
|
|
type CMSummary struct {
|
|
Namespace string `json:"namespace"`
|
|
Name string `json:"name"`
|
|
KeyCount int `json:"key_count"`
|
|
}
|
|
result := make([]CMSummary, 0, len(cms.Items))
|
|
for _, cm := range cms.Items {
|
|
result = append(result, CMSummary{
|
|
Namespace: cm.Metadata.Namespace,
|
|
Name: cm.Metadata.Name,
|
|
KeyCount: len(cm.Data),
|
|
})
|
|
}
|
|
c.JSON(http.StatusOK, model.Response{Code: 200, Data: result})
|
|
}
|
|
|
|
// ListSecrets 获取命名空间下的 Secret 列表(仅名称+类型,不含值)
|
|
func (h *KubernetesHandler) ListSecrets(c *gin.Context) {
|
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
|
if clusterID == 0 {
|
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
|
}
|
|
namespace := c.Param("namespace")
|
|
|
|
client, _, err := h.getK8sClient(clusterID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
secrets, err := client.ListSecrets(namespace)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
// 扁平化字段,与前端表格列 name/namespace/type 对齐 // Logan.Li
|
|
type SecretSummary struct {
|
|
Namespace string `json:"namespace"`
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
KeyCount int `json:"key_count"`
|
|
}
|
|
result := make([]SecretSummary, 0, len(secrets.Items))
|
|
for _, s := range secrets.Items {
|
|
result = append(result, SecretSummary{
|
|
Namespace: s.Metadata.Namespace,
|
|
Name: s.Metadata.Name,
|
|
Type: s.Type,
|
|
KeyCount: len(s.DataKeys),
|
|
})
|
|
}
|
|
c.JSON(http.StatusOK, model.Response{Code: 200, Data: result})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Helpers
|
|
// ============================================================================
|
|
|
|
// resolveClusterCode resolves a cluster code to its numeric ID
|
|
func (h *KubernetesHandler) ListServicesNS(c *gin.Context) {
|
|
h.listServicesWithNS(c, c.Param("namespace"))
|
|
}
|
|
func (h *KubernetesHandler) ListIngressesNS(c *gin.Context) {
|
|
h.listIngressesWithNS(c, c.Param("namespace"))
|
|
}
|
|
func (h *KubernetesHandler) listServicesWithNS(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 }
|
|
svcs, err := client.ListServices()
|
|
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
|
type SvcPortDetail struct {
|
|
Name string `json:"name,omitempty"`
|
|
Port int `json:"port"`
|
|
TargetPort interface{} `json:"targetPort"`
|
|
NodePort int `json:"nodePort,omitempty"`
|
|
Protocol string `json:"protocol,omitempty"`
|
|
}
|
|
type SvcSummary struct {
|
|
Namespace string `json:"namespace"`
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
ClusterIP string `json:"cluster_ip"`
|
|
Ports int `json:"ports"`
|
|
PortList []SvcPortDetail `json:"portList,omitempty"`
|
|
Selector map[string]string `json:"selector,omitempty"`
|
|
}
|
|
result := []SvcSummary{}
|
|
for _, s := range svcs.Items {
|
|
if s.Metadata.Namespace != ns { continue }
|
|
ports := []SvcPortDetail{}
|
|
for _, p := range s.Spec.Ports {
|
|
ports = append(ports, SvcPortDetail{
|
|
Name: p.Name, Port: p.Port, TargetPort: p.TargetPort,
|
|
NodePort: p.NodePort, Protocol: p.Protocol,
|
|
})
|
|
}
|
|
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),
|
|
PortList: ports,
|
|
Selector: s.Spec.Selector,
|
|
})
|
|
}
|
|
c.JSON(200, model.Response{Code: 200, Data: result})
|
|
}
|
|
func (h *KubernetesHandler) listIngressesWithNS(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 }
|
|
ings, err := client.ListIngresses()
|
|
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
|
type PathDetail struct {
|
|
Path string `json:"path"`
|
|
PathType string `json:"pathType,omitempty"`
|
|
BackendSvc string `json:"backendService,omitempty"`
|
|
BackendPort string `json:"backendPort,omitempty"`
|
|
}
|
|
type RuleDetail struct {
|
|
Host string `json:"host"`
|
|
Paths []PathDetail `json:"paths"`
|
|
}
|
|
type IngSummary struct {
|
|
Namespace string `json:"namespace"`
|
|
Name string `json:"name"`
|
|
Hosts []string `json:"hosts"`
|
|
TLS bool `json:"tls"`
|
|
IngressClass string `json:"ingressClass,omitempty"`
|
|
Rules []RuleDetail `json:"rules,omitempty"`
|
|
}
|
|
result := []IngSummary{}
|
|
for _, ing := range ings.Items {
|
|
if ing.Metadata.Namespace != ns { continue }
|
|
hosts := []string{}
|
|
rules := []RuleDetail{}
|
|
for _, rule := range ing.Spec.Rules {
|
|
if rule.Host != "" { hosts = append(hosts, rule.Host) }
|
|
rd := RuleDetail{Host: rule.Host, Paths: []PathDetail{}}
|
|
if rule.HTTP != nil {
|
|
for _, p := range rule.HTTP.Paths {
|
|
pd := PathDetail{Path: p.Path, PathType: p.PathType, BackendSvc: p.Backend.Service.Name}
|
|
if p.Backend.Service.Port.Number > 0 {
|
|
pd.BackendPort = strconv.Itoa(p.Backend.Service.Port.Number)
|
|
} else if p.Backend.Service.Port.Name != "" {
|
|
pd.BackendPort = p.Backend.Service.Port.Name
|
|
}
|
|
rd.Paths = append(rd.Paths, pd)
|
|
}
|
|
}
|
|
rules = append(rules, rd)
|
|
}
|
|
// ingressClassName from annotations
|
|
ingClass := ""
|
|
if ing.Metadata.Annotations != nil {
|
|
ingClass = ing.Metadata.Annotations["kubernetes.io/ingress.class"]
|
|
}
|
|
result = append(result, IngSummary{
|
|
Namespace: ing.Metadata.Namespace,
|
|
Name: ing.Metadata.Name,
|
|
Hosts: hosts,
|
|
TLS: len(ing.Spec.TLS) > 0,
|
|
IngressClass: ingClass,
|
|
Rules: rules,
|
|
})
|
|
}
|
|
c.JSON(200, model.Response{Code: 200, Data: result})
|
|
}
|
|
|
|
// ListIngressClasses 列出集群所有 IngressClass(供可视化编辑 Ingress 时下拉选择) // Logan.Li
|
|
func (h *KubernetesHandler) ListIngressClasses(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
|
|
}
|
|
icList, err := client.ListIngressClasses()
|
|
if err != nil {
|
|
c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
type ICItem struct {
|
|
Name string `json:"name"`
|
|
Controller string `json:"controller"`
|
|
}
|
|
result := []ICItem{}
|
|
for _, ic := range icList.Items {
|
|
result = append(result, ICItem{
|
|
Name: ic.Metadata.Name,
|
|
Controller: ic.Spec.Controller,
|
|
})
|
|
}
|
|
c.JSON(200, model.Response{Code: 200, Data: result})
|
|
}
|
|
|
|
func (h *KubernetesHandler) ListPVCsNS(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")
|
|
pvcs, err := client.ListPVCs(ns)
|
|
if err != nil { c.JSON(500, model.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
|
type PVCSummary struct {
|
|
Namespace string `json:"namespace"`
|
|
Name string `json:"name"`
|
|
Phase string `json:"phase"`
|
|
Size string `json:"size"`
|
|
StorageClass string `json:"storage_class"`
|
|
}
|
|
result := []PVCSummary{}
|
|
for _, p := range pvcs.Items {
|
|
size := ""
|
|
if p.Status.Capacity != nil {
|
|
if s, ok := p.Status.Capacity["storage"]; ok { size = s }
|
|
}
|
|
result = append(result, PVCSummary{
|
|
Namespace: p.Metadata.Namespace,
|
|
Name: p.Metadata.Name,
|
|
Phase: p.Status.Phase,
|
|
Size: size,
|
|
StorageClass: p.Spec.StorageClassName,
|
|
})
|
|
}
|
|
c.JSON(200, model.Response{Code: 200, Data: result})
|
|
}
|
|
|
|
// DeletePVCNS deletes a PVC (code-based)
|
|
func (h *KubernetesHandler) DeletePVCNS(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 }
|
|
if err := client.DeletePVC(c.Param("namespace"), c.Param("name")); err != nil {
|
|
c.JSON(500, model.Response{Code: 500, Message: "删除失败: " + err.Error()}); return
|
|
}
|
|
c.JSON(200, model.Response{Code: 200, Message: "已删除"})
|
|
}
|
|
|
|
// ============================================================================
|
|
// StatefulSet / DaemonSet handlers (code-based)
|
|
// ============================================================================
|
|
|
|
// ListStatefulSetsNS lists StatefulSets in a namespace
|