后端: - pkg/k8sclient → pkg/kubernetesclient - models/k8s.go → models/kubernetes.go - handlers/k8s.go → handlers/kubernetes.go - K8sCluster/K8sNode/.. → KubernetesCluster/KubernetesNode/.. - K8sHandler → KubernetesHandler - 路由: /k8s-clusters → /kubernetes-clusters - 路由: /k8s/:code/namespace/:ns/... → /kubernetes/:code/namespace/:ns/... (Kuboard风格) - 表前缀: container_k8s_ → container_kubernetes_ 前端: - components/k8s/ → components/kubernetes/ - K8sClusters → KubernetesClusters - K8sWorkloads → KubernetesWorkloads - K8sNodes → KubernetesNodes - k8sApi → kubernetesApi - 路由前缀: /k8s/ → /kubernetes/ 测试验证: ✅ /kubernetes/k8s-cluster-liren/namespace/kube-system/deployments → 3 items ✅ /kubernetes/k8s-cluster-liren/namespace/kube-system/pods → 7 items ✅ /kubernetes/k8s-cluster-liren/namespace/kube-system/services → 4 items ✅ /kubernetes/k8s-cluster-liren/namespace/kube-system/configmaps → 6 items ✅ /kubernetes/k8s-cluster-liren/namespace/kube-system/secrets → 1 item ✅ /kubernetes/k8s-cluster-liren/namespaces → 4 items
1712 lines
58 KiB
Go
1712 lines
58 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"ezops/internal/models"
|
|
"ezops/pkg/kubernetesclient"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// KubernetesHandler K8s处理器
|
|
type KubernetesHandler struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
// NewKubernetesHandler 创建K8s处理器
|
|
func NewKubernetesHandler(db *gorm.DB) *KubernetesHandler {
|
|
return &KubernetesHandler{db: db}
|
|
}
|
|
|
|
// ListClusters 获取集群列表
|
|
// @Summary 获取集群列表
|
|
// @Description 获取所有K8s集群列表
|
|
// @Tags K8s集群
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param page query int false "页码"
|
|
// @Param page_size query int false "每页数量"
|
|
// @Success 200 {object} models.Response{data=models.ListResponse}
|
|
// @Router /kubernetes-clusters [get]
|
|
func (h *KubernetesHandler) ListClusters(c *gin.Context) {
|
|
var query models.PageQuery
|
|
c.ShouldBindQuery(&query)
|
|
|
|
var total int64
|
|
h.db.Model(&models.KubernetesCluster{}).Count(&total)
|
|
|
|
var clusters []models.KubernetesCluster
|
|
h.db.Offset(query.GetOffset()).Limit(query.GetLimit()).Find(&clusters)
|
|
|
|
c.JSON(http.StatusOK, models.Response{
|
|
Code: 200,
|
|
Message: "success",
|
|
Data: models.ListResponse{
|
|
List: clusters,
|
|
Total: total,
|
|
},
|
|
})
|
|
}
|
|
|
|
// GetCluster 获取集群详情
|
|
// @Summary 获取集群详情
|
|
// @Description 获取指定K8s集群的详细信息
|
|
// @Tags K8s集群
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param id path int true "集群ID"
|
|
// @Success 200 {object} models.Response{data=models.KubernetesCluster}
|
|
// @Router /kubernetes-clusters/{id} [get]
|
|
func (h *KubernetesHandler) GetCluster(c *gin.Context) {
|
|
id, _ := strconv.Atoi(c.Param("id"))
|
|
|
|
var cluster models.KubernetesCluster
|
|
if err := h.db.First(&cluster, id).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, models.Response{
|
|
Code: 404,
|
|
Message: "集群不存在",
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, models.Response{
|
|
Code: 200,
|
|
Message: "success",
|
|
Data: cluster,
|
|
})
|
|
}
|
|
|
|
// CreateCluster 创建集群
|
|
// @Summary 创建集群
|
|
// @Description 创建新K8s集群
|
|
// @Tags K8s集群
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param request body models.CreateKubernetesClusterRequest true "创建请求"
|
|
// @Success 200 {object} models.Response{data=models.KubernetesCluster}
|
|
// @Router /kubernetes-clusters [post]
|
|
func (h *KubernetesHandler) CreateCluster(c *gin.Context) {
|
|
var req models.CreateKubernetesClusterRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, models.Response{
|
|
Code: 400,
|
|
Message: "请求参数错误: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
cluster := models.KubernetesCluster{
|
|
Name: req.Name,
|
|
Code: req.Code,
|
|
Version: req.Version,
|
|
ConfigType: req.ConfigType,
|
|
APIServer: req.APIServer,
|
|
APIEndpoint: req.APIServer, // auto-sync
|
|
Token: req.Token,
|
|
KubeConfig: req.KubeConfig,
|
|
Description: req.Description,
|
|
Status: "healthy",
|
|
}
|
|
|
|
if err := h.db.Create(&cluster).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{
|
|
Code: 500,
|
|
Message: "创建失败",
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, models.Response{
|
|
Code: 200,
|
|
Message: "创建成功",
|
|
Data: cluster,
|
|
})
|
|
}
|
|
|
|
// UpdateCluster 更新集群
|
|
// @Summary 更新集群
|
|
// @Description 更新指定K8s集群信息
|
|
// @Tags K8s集群
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param id path int true "集群ID"
|
|
// @Param request body models.CreateKubernetesClusterRequest true "更新请求"
|
|
// @Success 200 {object} models.Response{data=models.KubernetesCluster}
|
|
// @Router /kubernetes-clusters/{id} [put]
|
|
func (h *KubernetesHandler) UpdateCluster(c *gin.Context) {
|
|
id, _ := strconv.Atoi(c.Param("id"))
|
|
|
|
// 用 map 接收,避免 binding:"required" 校验 // Logan.Li
|
|
var req map[string]interface{}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, models.Response{
|
|
Code: 400,
|
|
Message: "请求参数错误: " + err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
var cluster models.KubernetesCluster
|
|
if err := h.db.First(&cluster, id).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, models.Response{
|
|
Code: 404,
|
|
Message: "集群不存在",
|
|
})
|
|
return
|
|
}
|
|
|
|
// 逐字段更新:空值不覆盖原值(防止凭据被清空) // Logan.Li
|
|
updates := map[string]interface{}{}
|
|
if v, ok := req["name"]; ok && v != nil && v != "" {
|
|
updates["name"] = v
|
|
}
|
|
if v, ok := req["version"]; ok {
|
|
updates["version"] = v
|
|
}
|
|
if v, ok := req["description"]; ok {
|
|
updates["description"] = v
|
|
}
|
|
if v, ok := req["config_type"]; ok {
|
|
updates["config_type"] = v
|
|
}
|
|
if v, ok := req["api_server"]; ok && v != nil && v != "" {
|
|
updates["api_server"] = v
|
|
updates["api_endpoint"] = v
|
|
}
|
|
// Token / KubeConfig:空值不覆盖(前端编辑时可能拿不到原值)
|
|
if v, ok := req["token"]; ok && v != nil && v != "" {
|
|
updates["token"] = v
|
|
}
|
|
if v, ok := req["kube_config"]; ok && v != nil && v != "" {
|
|
updates["kube_config"] = v
|
|
}
|
|
|
|
if len(updates) == 0 {
|
|
c.JSON(http.StatusOK, models.Response{
|
|
Code: 200,
|
|
Message: "无更新内容",
|
|
Data: cluster,
|
|
})
|
|
return
|
|
}
|
|
|
|
if err := h.db.Model(&cluster).Updates(updates).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{
|
|
Code: 500,
|
|
Message: "更新失败",
|
|
})
|
|
return
|
|
}
|
|
|
|
h.db.First(&cluster, id)
|
|
|
|
c.JSON(http.StatusOK, models.Response{
|
|
Code: 200,
|
|
Message: "更新成功",
|
|
Data: cluster,
|
|
})
|
|
}
|
|
|
|
// GetClusterDetail 返回集群完整信息(含 token/kube_config,供编辑回填) // Logan.Li
|
|
func (h *KubernetesHandler) GetClusterDetail(c *gin.Context) {
|
|
id, _ := strconv.Atoi(c.Param("id"))
|
|
|
|
var cluster models.KubernetesCluster
|
|
if err := h.db.First(&cluster, id).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, models.Response{
|
|
Code: 404,
|
|
Message: "集群不存在",
|
|
})
|
|
return
|
|
}
|
|
|
|
// 返回时手动脱敏:token/kube_config 只返回是否存在标记,不返回原文
|
|
c.JSON(http.StatusOK, models.Response{
|
|
Code: 200,
|
|
Message: "success",
|
|
Data: map[string]interface{}{
|
|
"id": cluster.ID,
|
|
"name": cluster.Name,
|
|
"code": cluster.Code,
|
|
"version": cluster.Version,
|
|
"status": cluster.Status,
|
|
"config_type": cluster.ConfigType,
|
|
"api_server": cluster.APIServer,
|
|
"api_endpoint": cluster.APIEndpoint,
|
|
"description": cluster.Description,
|
|
"has_token": cluster.Token != "",
|
|
"has_kube_config": cluster.KubeConfig != "",
|
|
"node_count": cluster.NodeCount,
|
|
"pod_count": cluster.PodCount,
|
|
"labels": cluster.Labels,
|
|
"created_at": cluster.CreatedAt,
|
|
"updated_at": cluster.UpdatedAt,
|
|
},
|
|
})
|
|
}
|
|
|
|
// DeleteCluster 删除集群
|
|
// @Summary 删除集群
|
|
// @Description 删除指定K8s集群
|
|
// @Tags K8s集群
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param id path int true "集群ID"
|
|
// @Success 200 {object} models.Response
|
|
// @Router /kubernetes-clusters/{id} [delete]
|
|
func (h *KubernetesHandler) DeleteCluster(c *gin.Context) {
|
|
id, _ := strconv.Atoi(c.Param("id"))
|
|
|
|
if err := h.db.Delete(&models.KubernetesCluster{}, id).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{
|
|
Code: 500,
|
|
Message: "删除失败",
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, models.Response{
|
|
Code: 200,
|
|
Message: "删除成功",
|
|
})
|
|
}
|
|
|
|
// ListNodes 获取节点列表
|
|
// @Summary 获取节点列表
|
|
// @Description 获取指定集群的节点列表
|
|
// @Tags K8s集群
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param id path int true "集群ID"
|
|
// @Success 200 {object} models.Response{data=[]models.KubernetesNode}
|
|
// @Router /kubernetes-clusters/{id}/nodes [get]
|
|
func (h *KubernetesHandler) ListNodes(c *gin.Context) {
|
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
|
|
|
client, _, err := h.getK8sClient(clusterID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
|
return
|
|
}
|
|
nodes, err := client.ListNodes()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
// Convert to simplified response
|
|
type NodeSummary struct {
|
|
Name string `json:"name"`
|
|
IP string `json:"ip"`
|
|
Role string `json:"role"`
|
|
Status string `json:"status"`
|
|
OS string `json:"os"`
|
|
Kubelet string `json:"kubelet_version"`
|
|
CPU int `json:"cpu"`
|
|
Memory int64 `json:"memory_mb"`
|
|
MaxPods int `json:"max_pods"`
|
|
}
|
|
result := []NodeSummary{}
|
|
for _, n := range nodes.Items {
|
|
role := "worker"
|
|
for k := range n.Metadata.Labels {
|
|
if k == "node-role.kubernetes.io/master" || k == "node-role.kubernetes.io/control-plane" {
|
|
role = "master"
|
|
break
|
|
}
|
|
}
|
|
nodeStatus := "NotReady"
|
|
for _, cond := range n.Status.Conditions {
|
|
if cond.Type == "Ready" && cond.Status == "True" {
|
|
nodeStatus = "Ready"
|
|
break
|
|
}
|
|
}
|
|
ip := ""
|
|
for _, addr := range n.Status.Addresses {
|
|
if addr.Type == "InternalIP" {
|
|
ip = addr.Address
|
|
break
|
|
}
|
|
}
|
|
result = append(result, NodeSummary{
|
|
Name: n.Metadata.Name,
|
|
IP: ip,
|
|
Role: role,
|
|
Status: nodeStatus,
|
|
OS: n.Status.NodeInfo.OSImage,
|
|
Kubelet: n.Status.NodeInfo.KubeletVersion,
|
|
CPU: parseIntResource(n.Status.Capacity["cpu"]),
|
|
Memory: parseMemResource(n.Status.Capacity["memory"]),
|
|
MaxPods: parseIntResource(n.Status.Capacity["pods"]),
|
|
})
|
|
}
|
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: result})
|
|
}
|
|
|
|
// ListPods 获取Pod列表
|
|
// @Summary 获取Pod列表
|
|
// @Description 获取指定集群的Pod列表
|
|
// @Tags K8s集群
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param id path int true "集群ID"
|
|
// @Param namespace query string false "命名空间"
|
|
// @Success 200 {object} models.Response{data=[]models.KubernetesPod}
|
|
// @Router /kubernetes-clusters/{id}/pods [get]
|
|
func (h *KubernetesHandler) ListPods(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, models.Response{Code: 500, Message: err.Error()})
|
|
return
|
|
}
|
|
pods, err := client.ListPods()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
type PodSummary struct {
|
|
Namespace string `json:"namespace"`
|
|
Name string `json:"name"`
|
|
Status string `json:"status"`
|
|
NodeName string `json:"node_name"`
|
|
IP string `json:"ip"`
|
|
RestartCount int `json:"restart_count"`
|
|
Image string `json:"image"`
|
|
}
|
|
result := []PodSummary{}
|
|
for _, p := range pods.Items {
|
|
if namespace != "" && p.Metadata.Namespace != namespace {
|
|
continue
|
|
}
|
|
image := ""
|
|
if len(p.Spec.Containers) > 0 {
|
|
image = p.Spec.Containers[0].Image
|
|
}
|
|
restarts := 0
|
|
if len(p.Status.ContainerStatuses) > 0 {
|
|
restarts = p.Status.ContainerStatuses[0].RestartCount
|
|
}
|
|
result = append(result, PodSummary{
|
|
Namespace: p.Metadata.Namespace,
|
|
Name: p.Metadata.Name,
|
|
Status: p.Status.Phase,
|
|
NodeName: p.Spec.NodeName,
|
|
IP: p.Status.PodIP,
|
|
RestartCount: restarts,
|
|
Image: image,
|
|
})
|
|
}
|
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: result})
|
|
}
|
|
|
|
// ListDeployments 获取Deployment列表
|
|
// @Summary 获取Deployment列表
|
|
// @Description 获取指定集群的Deployment列表
|
|
// @Tags K8s集群
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param id path int true "集群ID"
|
|
// @Param namespace query string false "命名空间"
|
|
// @Success 200 {object} models.Response{data=[]models.KubernetesDeployment}
|
|
// @Router /kubernetes-clusters/{id}/deployments [get]
|
|
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, models.Response{Code: 500, Message: err.Error()})
|
|
return
|
|
}
|
|
deps, err := client.ListDeployments()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.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, models.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} models.Response{data=[]models.KubernetesService}
|
|
// @Router /kubernetes-clusters/{id}/services [get]
|
|
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, models.Response{Code: 500, Message: err.Error()})
|
|
return
|
|
}
|
|
svcs, err := client.ListServices()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.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, models.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} models.Response{data=[]models.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, models.Response{Code: 500, Message: err.Error()})
|
|
return
|
|
}
|
|
ings, err := client.ListIngresses()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.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, models.Response{Code: 200, Message: "success", Data: result})
|
|
}
|
|
|
|
// GetTopology 获取集群拓扑布局 // Logan.Li
|
|
func (h *KubernetesHandler) GetTopology(c *gin.Context) {
|
|
clusterID := c.Param("id")
|
|
var layout models.TopologyLayout
|
|
result := h.db.Where("cluster_id = ?", clusterID).First(&layout)
|
|
if result.Error != nil {
|
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: &models.TopologyLayout{}})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, models.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, models.Response{Code: 400, Message: "参数错误"})
|
|
return
|
|
}
|
|
|
|
var layout models.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, models.Response{Code: 200, Message: "保存成功"})
|
|
}
|
|
|
|
|
|
// SyncCluster 同步集群资源(连接真实K8s API拉取数据)
|
|
func (h *KubernetesHandler) SyncCluster(c *gin.Context) {
|
|
id, _ := strconv.Atoi(c.Param("id"))
|
|
|
|
var cluster models.KubernetesCluster
|
|
if err := h.db.First(&cluster, id).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "集群不存在"})
|
|
return
|
|
}
|
|
|
|
if cluster.KubeConfig == "" && cluster.Token == "" {
|
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "集群未配置凭证"})
|
|
return
|
|
}
|
|
|
|
client, err := k8sclient.NewClient(cluster.KubeConfig)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "连接K8s失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
result := k8sclient.SyncResult{}
|
|
|
|
// Sync nodes
|
|
if nodes, err := client.ListNodes(); err == nil {
|
|
// Use raw SQL for reliability (avoid GORM soft-delete / auto-timestamp issues)
|
|
h.db.Exec("DELETE FROM kubernetes_nodes WHERE cluster_id = ?", cluster.ID)
|
|
for _, n := range nodes.Items {
|
|
role := "worker"
|
|
if _, ok := n.Metadata.Labels["node-role.kubernetes.io/master"]; ok {
|
|
role = "master"
|
|
} else if _, ok := n.Metadata.Labels["node-role.kubernetes.io/control-plane"]; ok {
|
|
role = "master"
|
|
}
|
|
nodeStatus := "NotReady"
|
|
for _, cond := range n.Status.Conditions {
|
|
if cond.Type == "Ready" && cond.Status == "True" {
|
|
nodeStatus = "Ready"
|
|
break
|
|
}
|
|
}
|
|
ip := ""
|
|
for _, addr := range n.Status.Addresses {
|
|
if addr.Type == "InternalIP" {
|
|
ip = addr.Address
|
|
break
|
|
}
|
|
}
|
|
labelsJSON, _ := json.Marshal(n.Metadata.Labels)
|
|
now := time.Now()
|
|
err2 := h.db.Exec(`INSERT INTO kubernetes_nodes (cluster_id, name, ip, role, status, os, kubelet_version, cpu, memory, max_pods, labels, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
cluster.ID, n.Metadata.Name, ip, role, nodeStatus, n.Status.NodeInfo.OSImage, n.Status.NodeInfo.KubeletVersion,
|
|
parseIntResource(n.Status.Capacity["cpu"]), parseMemResource(n.Status.Capacity["memory"]), parseIntResource(n.Status.Capacity["pods"]),
|
|
string(labelsJSON), now, now).Error
|
|
if err2 != nil {
|
|
log.Printf("[K8s] insert node %s error: %v", n.Metadata.Name, err2)
|
|
} else {
|
|
result.Nodes++
|
|
}
|
|
}
|
|
} else {
|
|
log.Printf("[K8s] sync nodes error: %v", err)
|
|
}
|
|
|
|
// Sync pods
|
|
if pods, err := client.ListPods(); err == nil {
|
|
h.db.Where("cluster_id = ?", cluster.ID).Delete(&models.KubernetesPod{})
|
|
for _, p := range pods.Items {
|
|
image := ""
|
|
if len(p.Spec.Containers) > 0 {
|
|
image = p.Spec.Containers[0].Image
|
|
}
|
|
restarts := 0
|
|
if len(p.Status.ContainerStatuses) > 0 {
|
|
restarts = p.Status.ContainerStatuses[0].RestartCount
|
|
}
|
|
labelsJSON, _ := json.Marshal(p.Metadata.Labels)
|
|
h.db.Create(&models.KubernetesPod{
|
|
ClusterID: cluster.ID,
|
|
Namespace: p.Metadata.Namespace,
|
|
Name: p.Metadata.Name,
|
|
Status: p.Status.Phase,
|
|
NodeName: p.Spec.NodeName,
|
|
IP: p.Status.PodIP,
|
|
Image: image,
|
|
RestartCount: restarts,
|
|
Labels: string(labelsJSON),
|
|
})
|
|
result.Pods++
|
|
}
|
|
} else {
|
|
log.Printf("[K8s] sync pods error: %v", err)
|
|
}
|
|
|
|
// Sync deployments
|
|
if deps, err := client.ListDeployments(); err == nil {
|
|
h.db.Where("cluster_id = ?", cluster.ID).Delete(&models.KubernetesDeployment{})
|
|
for _, d := range deps.Items {
|
|
image := ""
|
|
if len(d.Spec.Template.Spec.Containers) > 0 {
|
|
image = d.Spec.Template.Spec.Containers[0].Image
|
|
}
|
|
labelsJSON, _ := json.Marshal(d.Metadata.Labels)
|
|
h.db.Create(&models.KubernetesDeployment{
|
|
ClusterID: cluster.ID,
|
|
Namespace: d.Metadata.Namespace,
|
|
Name: d.Metadata.Name,
|
|
Replicas: d.Spec.Replicas,
|
|
AvailableReplicas: d.Status.AvailableReplicas,
|
|
ReadyReplicas: d.Status.ReadyReplicas,
|
|
Image: image,
|
|
Labels: string(labelsJSON),
|
|
})
|
|
result.Deployments++
|
|
}
|
|
} else {
|
|
log.Printf("[K8s] sync deps error: %v", err)
|
|
}
|
|
|
|
// Sync services
|
|
if svcs, err := client.ListServices(); err == nil {
|
|
h.db.Where("cluster_id = ?", cluster.ID).Delete(&models.KubernetesService{})
|
|
for _, s := range svcs.Items {
|
|
portsJSON, _ := json.Marshal(s.Spec.Ports)
|
|
selectorJSON, _ := json.Marshal(s.Spec.Selector)
|
|
labelsJSON, _ := json.Marshal(s.Metadata.Labels)
|
|
h.db.Create(&models.KubernetesService{
|
|
ClusterID: cluster.ID,
|
|
Namespace: s.Metadata.Namespace,
|
|
Name: s.Metadata.Name,
|
|
Type: s.Spec.Type,
|
|
ClusterIP: s.Spec.ClusterIP,
|
|
Ports: string(portsJSON),
|
|
Selector: string(selectorJSON),
|
|
Labels: string(labelsJSON),
|
|
})
|
|
result.Services++
|
|
}
|
|
}
|
|
|
|
// Sync ingresses
|
|
if ings, err := client.ListIngresses(); err == nil {
|
|
h.db.Where("cluster_id = ?", cluster.ID).Delete(&models.KubernetesIngress{})
|
|
for _, ing := range ings.Items {
|
|
hosts := []string{}
|
|
for _, rule := range ing.Spec.Rules {
|
|
hosts = append(hosts, rule.Host)
|
|
}
|
|
hostsJSON, _ := json.Marshal(hosts)
|
|
tls := len(ing.Spec.TLS) > 0
|
|
tlsHosts := []string{}
|
|
if tls {
|
|
for _, t := range ing.Spec.TLS {
|
|
tlsHosts = append(tlsHosts, t.Hosts...)
|
|
}
|
|
}
|
|
tlsHostsJSON, _ := json.Marshal(tlsHosts)
|
|
labelsJSON, _ := json.Marshal(ing.Metadata.Labels)
|
|
h.db.Create(&models.KubernetesIngress{
|
|
ClusterID: cluster.ID,
|
|
Namespace: ing.Metadata.Namespace,
|
|
Name: ing.Metadata.Name,
|
|
Hosts: string(hostsJSON),
|
|
TLS: tls,
|
|
TLSHosts: string(tlsHostsJSON),
|
|
Labels: string(labelsJSON),
|
|
})
|
|
result.Ingresses++
|
|
}
|
|
}
|
|
|
|
// Update cluster stats
|
|
now := time.Now()
|
|
h.db.Model(&cluster).Updates(map[string]interface{}{
|
|
"node_count": result.Nodes,
|
|
"pod_count": result.Pods,
|
|
"deployment_count": result.Deployments,
|
|
"service_count": result.Services,
|
|
"last_sync_at": &now,
|
|
})
|
|
|
|
c.JSON(http.StatusOK, models.Response{
|
|
Code: 200,
|
|
Message: fmt.Sprintf("同步完成: %d nodes, %d pods, %d deployments, %d services, %d ingresses",
|
|
result.Nodes, result.Pods, result.Deployments, result.Services, result.Ingresses),
|
|
Data: result,
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Pod Logs
|
|
// ============================================================================
|
|
|
|
// GetPodLogs 获取 Pod 日志(支持 stream=true 实时流)
|
|
func (h *KubernetesHandler) GetPodLogs(c *gin.Context) {
|
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
|
namespace := c.Param("namespace")
|
|
podName := c.Param("podName")
|
|
container := c.Query("container")
|
|
tailLines, _ := strconv.Atoi(c.DefaultQuery("tail", "200"))
|
|
stream := c.Query("stream") == "true"
|
|
|
|
client, _, err := h.getK8sClient(clusterID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
if stream {
|
|
// Real-time streaming via chunked transfer
|
|
body, err := client.GetPodLogsStream(namespace, podName, container, tailLines)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "获取日志流失败: " + err.Error()})
|
|
return
|
|
}
|
|
defer body.Close()
|
|
|
|
c.Header("Content-Type", "text/plain; charset=utf-8")
|
|
c.Header("Cache-Control", "no-cache")
|
|
c.Header("Connection", "keep-alive")
|
|
c.Header("X-Content-Type-Options", "nosniff")
|
|
|
|
c.Stream(func(w io.Writer) bool {
|
|
buf := make([]byte, 4096)
|
|
n, err := body.Read(buf)
|
|
if n > 0 {
|
|
w.Write(buf[:n])
|
|
return true
|
|
}
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
return
|
|
}
|
|
|
|
logs, err := client.GetPodLogs(namespace, podName, container, tailLines)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "获取日志失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: gin.H{"logs": logs, "pod": podName, "namespace": namespace}})
|
|
}
|
|
|
|
// ============================================================================
|
|
// WebSocket Exec Terminal (交互式 bash)
|
|
// ============================================================================
|
|
|
|
// ExecTerminal WebSocket 终端 — 前端 WS → 后端 → K8s SPDY exec
|
|
func (h *KubernetesHandler) ExecTerminal(c *gin.Context) {
|
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
|
namespace := c.Param("namespace")
|
|
podName := c.Param("podName")
|
|
container := c.Query("container")
|
|
command := c.Query("command")
|
|
if command == "" {
|
|
command = "sh"
|
|
}
|
|
|
|
client, _, err := h.getK8sClient(clusterID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
// Build exec URL with SPDY upgrade
|
|
execPath := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/exec?command=%s&stdin=true&stdout=true&stderr=true&tty=true",
|
|
namespace, podName, command)
|
|
if container != "" {
|
|
execPath += "&container=" + container
|
|
}
|
|
|
|
// Open SPDY connection to K8s API
|
|
k8sReq, err := http.NewRequest("POST", client.BaseURL+execPath, nil)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "build request: " + err.Error()})
|
|
return
|
|
}
|
|
k8sReq.Header.Set("Connection", "Upgrade")
|
|
k8sReq.Header.Set("Upgrade", "SPDY/3.1")
|
|
k8sReq.Header.Set("X-Stream-Protocol-Version", "v4.channel.k8s.io")
|
|
k8sReq.Header.Set("X-Stream-Protocol-Version", "v3.channel.k8s.io")
|
|
|
|
// Use raw TCP dial for SPDY upgrade
|
|
dialer := client.HTTPClient.Transport.(*http.Transport).DialContext
|
|
conn, err := dialer(c.Request.Context(), "tcp", strings.TrimPrefix(strings.TrimPrefix(client.BaseURL, "https://"), "http://"))
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "dial K8s: " + err.Error()})
|
|
return
|
|
}
|
|
defer conn.Close()
|
|
|
|
if err := k8sReq.Write(conn); err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "write request: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
// Read the 101 Switching Protocols response
|
|
buf := make([]byte, 4096)
|
|
n, err := conn.Read(buf)
|
|
if err != nil || n == 0 {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "read upgrade response: " + fmt.Sprint(err)})
|
|
return
|
|
}
|
|
respStr := string(buf[:n])
|
|
if !strings.Contains(respStr, "101") {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "upgrade failed: " + respStr[:min(len(respStr), 200)]})
|
|
return
|
|
}
|
|
|
|
// Upgrade Gin to WebSocket
|
|
wsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer wsConn.Close()
|
|
|
|
// Bidirectional proxy: WS ↔ SPDY
|
|
done := make(chan struct{}, 2)
|
|
|
|
// WS → SPDY (stdin)
|
|
go func() {
|
|
defer func() { done <- struct{}{} }()
|
|
for {
|
|
_, msg, err := wsConn.ReadMessage()
|
|
if err != nil {
|
|
return
|
|
}
|
|
// SPDY stdin frame: channel 0
|
|
frame := make([]byte, len(msg)+8)
|
|
frame[0] = 0 // channel 0 (stdin)
|
|
copy(frame[8:], msg)
|
|
if _, err := conn.Write(frame); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
// SPDY → WS (stdout/stderr)
|
|
go func() {
|
|
defer func() { done <- struct{}{} }()
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
n, err := conn.Read(buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if n > 8 {
|
|
// Skip SPDY header (8 bytes), send data
|
|
wsConn.WriteMessage(1, buf[8:n]) // 1 = TextMessage
|
|
}
|
|
}
|
|
}()
|
|
|
|
<-done
|
|
}
|
|
|
|
// ============================================================================
|
|
// Exec / File Browser
|
|
// ============================================================================
|
|
|
|
// ExecInPod 在 Pod 中执行命令 (文件浏览 / 单次命令)
|
|
func (h *KubernetesHandler) ExecInPod(c *gin.Context) {
|
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
|
namespace := c.Param("namespace")
|
|
podName := c.Param("podName")
|
|
|
|
var req struct {
|
|
Command []string `json:"command"`
|
|
Container string `json:"container"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误"})
|
|
return
|
|
}
|
|
if len(req.Command) == 0 {
|
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "command 不能为空"})
|
|
return
|
|
}
|
|
|
|
client, _, err := h.getK8sClient(clusterID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
result, err := client.ExecInPod(namespace, podName, req.Container, req.Command)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "执行失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: result})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Scale (副本数加减)
|
|
// ============================================================================
|
|
|
|
// ScaleDeployment 缩放 Deployment 副本数
|
|
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, models.Response{Code: 400, Message: "参数错误"})
|
|
return
|
|
}
|
|
if req.Replicas < 0 {
|
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "副本数不能为负数"})
|
|
return
|
|
}
|
|
|
|
client, _, err := h.getK8sClient(clusterID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
if err := client.ScaleDeployment(namespace, name, req.Replicas); err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "扩缩容失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, models.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, models.Response{Code: 500, Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
if err := client.RestartDeployment(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 管理
|
|
// ============================================================================
|
|
|
|
// ListHPAs 获取 HPA 列表
|
|
func (h *KubernetesHandler) ListHPAs(c *gin.Context) {
|
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
|
|
|
client, _, err := h.getK8sClient(clusterID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
hpas, err := client.ListHPAs()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: hpas.Items})
|
|
}
|
|
|
|
// CreateHPA 创建 HPA
|
|
func (h *KubernetesHandler) CreateHPA(c *gin.Context) {
|
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
|
|
|
var req k8sclient.CreateHPARequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
|
return
|
|
}
|
|
if req.Name == "" || req.Namespace == "" || req.TargetName == "" {
|
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "name/namespace/targetName 必填"})
|
|
return
|
|
}
|
|
|
|
client, _, err := h.getK8sClient(clusterID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
hpa, err := client.CreateHPA(req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建 HPA 失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "HPA 创建成功", Data: hpa})
|
|
}
|
|
|
|
// DeleteHPA 删除 HPA
|
|
func (h *KubernetesHandler) DeleteHPA(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.DeleteHPA(namespace, name); err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除 HPA 失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "HPA 已删除"})
|
|
}
|
|
|
|
// ============================================================================
|
|
// CronJob 管理
|
|
// ============================================================================
|
|
|
|
// ListCronJobs 获取 CronJob 列表
|
|
func (h *KubernetesHandler) ListCronJobs(c *gin.Context) {
|
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
|
|
|
client, _, err := h.getK8sClient(clusterID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
cronjobs, err := client.ListCronJobs()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: cronjobs.Items})
|
|
}
|
|
|
|
// CreateCronJob 创建 CronJob
|
|
func (h *KubernetesHandler) CreateCronJob(c *gin.Context) {
|
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
|
|
|
var req k8sclient.CreateCronJobRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
client, _, err := h.getK8sClient(clusterID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
cj, err := client.CreateCronJob(req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建 CronJob 失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "CronJob 创建成功", Data: cj})
|
|
}
|
|
|
|
// SuspendCronJob 暂停/恢复 CronJob
|
|
func (h *KubernetesHandler) SuspendCronJob(c *gin.Context) {
|
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
|
namespace := c.Param("namespace")
|
|
name := c.Param("name")
|
|
|
|
var req struct {
|
|
Suspend bool `json:"suspend"`
|
|
}
|
|
c.ShouldBindJSON(&req)
|
|
|
|
client, _, err := h.getK8sClient(clusterID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
if err := client.SuspendCronJob(namespace, name, req.Suspend); err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "操作失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
action := "恢复"
|
|
if req.Suspend {
|
|
action = "暂停"
|
|
}
|
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "CronJob 已" + action})
|
|
}
|
|
|
|
// DeleteCronJob 删除 CronJob
|
|
func (h *KubernetesHandler) DeleteCronJob(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.DeleteCronJob(namespace, name); err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除 CronJob 失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "CronJob 已删除"})
|
|
}
|
|
|
|
// TriggerCronJob 手动触发 CronJob
|
|
func (h *KubernetesHandler) TriggerCronJob(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.TriggerCronJob(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: "手动触发成功"})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Deployment CI 集成 (Webhook 更新镜像)
|
|
// ============================================================================
|
|
|
|
// UpdateDeploymentImage 更新 Deployment 容器镜像 (CI webhook)
|
|
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, models.Response{Code: 400, Message: "参数错误"})
|
|
return
|
|
}
|
|
if req.Image == "" {
|
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "image 不能为空"})
|
|
return
|
|
}
|
|
|
|
client, _, err := h.getK8sClient(clusterID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
if err := client.UpdateDeploymentImage(namespace, name, req.ContainerName, req.Image); err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "更新镜像失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: fmt.Sprintf("镜像已更新为 %s,正在滚动更新...", req.Image)})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Namespace
|
|
// ============================================================================
|
|
|
|
// ListNamespaces 获取命名空间列表
|
|
func (h *KubernetesHandler) ListNamespaces(c *gin.Context) {
|
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
|
// Support clusterCode param from new routes
|
|
if clusterID == 0 {
|
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
|
}
|
|
|
|
client, _, err := h.getK8sClient(clusterID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
namespaces, err := client.ListNamespaces()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: namespaces})
|
|
}
|
|
|
|
// ============================================================================
|
|
// ConfigMap & Secret (Kuboard-style namespace resources)
|
|
// ============================================================================
|
|
|
|
// ListConfigMaps 获取命名空间下的 ConfigMap 列表
|
|
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, models.Response{Code: 500, Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
cms, err := client.ListConfigMaps(namespace)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: cms.Items})
|
|
}
|
|
|
|
// 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, models.Response{Code: 500, Message: err.Error()})
|
|
return
|
|
}
|
|
|
|
secrets, err := client.ListSecrets(namespace)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: secrets.Items})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Helpers
|
|
// ============================================================================
|
|
|
|
// resolveClusterCode resolves a cluster code to its numeric ID
|
|
func (h *KubernetesHandler) resolveClusterCode(code string) int {
|
|
if code == "" {
|
|
return 0
|
|
}
|
|
var cluster models.KubernetesCluster
|
|
if err := h.db.Where("code = ?", code).First(&cluster).Error; err != nil {
|
|
return 0
|
|
}
|
|
return int(cluster.ID)
|
|
}
|
|
|
|
// getK8sClientByCode resolves cluster by code, falls back to ID
|
|
func (h *KubernetesHandler) getK8sClientByCode(c *gin.Context) (*k8sclient.Client, *models.KubernetesCluster, error) {
|
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
|
if clusterID == 0 {
|
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
|
}
|
|
if clusterID == 0 {
|
|
return nil, nil, fmt.Errorf("cluster not found")
|
|
}
|
|
return h.getK8sClient(clusterID)
|
|
}
|
|
|
|
// ============================================================================
|
|
// NS wrappers — read namespace from path (Kuboard-style routes)
|
|
// ============================================================================
|
|
|
|
func (h *KubernetesHandler) ListDeploymentsNS(c *gin.Context) {
|
|
h.listDeploymentsWithNS(c, c.Param("namespace"))
|
|
}
|
|
func (h *KubernetesHandler) ListPodsNS(c *gin.Context) {
|
|
h.listPodsWithNS(c, c.Param("namespace"))
|
|
}
|
|
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) ListHPAsNS(c *gin.Context) {
|
|
h.listHPAsWithNS(c, c.Param("namespace"))
|
|
}
|
|
func (h *KubernetesHandler) CreateHPANS(c *gin.Context) {
|
|
h.createHPAWithNS(c, c.Param("namespace"))
|
|
}
|
|
func (h *KubernetesHandler) DeleteHPANS(c *gin.Context) {
|
|
h.deleteHPAWithNS(c, c.Param("namespace"))
|
|
}
|
|
func (h *KubernetesHandler) ListCronJobsNS(c *gin.Context) {
|
|
h.listCronJobsWithNS(c, c.Param("namespace"))
|
|
}
|
|
func (h *KubernetesHandler) CreateCronJobNS(c *gin.Context) {
|
|
h.createCronJobWithNS(c, c.Param("namespace"))
|
|
}
|
|
func (h *KubernetesHandler) SuspendCronJobNS(c *gin.Context) {
|
|
h.suspendCronJobWithNS(c, c.Param("namespace"))
|
|
}
|
|
func (h *KubernetesHandler) TriggerCronJobNS(c *gin.Context) {
|
|
h.triggerCronJobWithNS(c, c.Param("namespace"))
|
|
}
|
|
func (h *KubernetesHandler) DeleteCronJobNS(c *gin.Context) {
|
|
h.deleteCronJobWithNS(c, c.Param("namespace"))
|
|
}
|
|
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) GetPodLogsNS(c *gin.Context) {
|
|
h.getPodLogsWithNS(c, c.Param("namespace"))
|
|
}
|
|
func (h *KubernetesHandler) ExecInPodNS(c *gin.Context) {
|
|
h.execInPodWithNS(c, c.Param("namespace"))
|
|
}
|
|
|
|
// Internal helpers that resolve clusterCode and inject 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, 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 }
|
|
type DepSummary struct {
|
|
Namespace, Name string; Replicas, AvailableReplicas, ReadyReplicas int; Image string
|
|
}
|
|
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{d.Metadata.Namespace, d.Metadata.Name, d.Spec.Replicas, d.Status.AvailableReplicas, d.Status.ReadyReplicas, image})
|
|
}
|
|
c.JSON(200, models.Response{Code: 200, Data: result})
|
|
}
|
|
func (h *KubernetesHandler) listPodsWithNS(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 }
|
|
pods, err := client.ListPods()
|
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
|
type PodSummary struct { Namespace, Name, Status, NodeName, IP string; RestartCount int; Image string }
|
|
result := []PodSummary{}
|
|
for _, p := range pods.Items {
|
|
if p.Metadata.Namespace != ns { continue }
|
|
image := ""; if len(p.Spec.Containers) > 0 { image = p.Spec.Containers[0].Image }
|
|
restarts := 0; if len(p.Status.ContainerStatuses) > 0 { restarts = p.Status.ContainerStatuses[0].RestartCount }
|
|
result = append(result, PodSummary{p.Metadata.Namespace, p.Metadata.Name, p.Status.Phase, p.Spec.NodeName, p.Status.PodIP, restarts, image})
|
|
}
|
|
c.JSON(200, models.Response{Code: 200, Data: result})
|
|
}
|
|
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, models.Response{Code: 500, Message: err.Error()}); return }
|
|
svcs, err := client.ListServices()
|
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
|
type SvcSummary struct { Namespace, Name, Type, ClusterIP string; Ports int }
|
|
result := []SvcSummary{}
|
|
for _, s := range svcs.Items {
|
|
if s.Metadata.Namespace != ns { continue }
|
|
result = append(result, SvcSummary{s.Metadata.Namespace, s.Metadata.Name, s.Spec.Type, s.Spec.ClusterIP, len(s.Spec.Ports)})
|
|
}
|
|
c.JSON(200, models.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, models.Response{Code: 500, Message: err.Error()}); return }
|
|
ings, err := client.ListIngresses()
|
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
|
type IngSummary struct { Namespace, Name string; Hosts []string; TLS bool }
|
|
result := []IngSummary{}
|
|
for _, ing := range ings.Items {
|
|
if ing.Metadata.Namespace != ns { continue }
|
|
hosts := []string{}
|
|
for _, rule := range ing.Spec.Rules { if rule.Host != "" { hosts = append(hosts, rule.Host) } }
|
|
result = append(result, IngSummary{ing.Metadata.Namespace, ing.Metadata.Name, hosts, len(ing.Spec.TLS) > 0})
|
|
}
|
|
c.JSON(200, models.Response{Code: 200, Data: result})
|
|
}
|
|
func (h *KubernetesHandler) listHPAsWithNS(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 }
|
|
hpas, err := client.ListHPAs()
|
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
|
filtered := []k8sclient.HPAItem{}
|
|
for _, h := range hpas.Items { if h.Metadata.Namespace == ns { filtered = append(filtered, h) } }
|
|
c.JSON(200, models.Response{Code: 200, Data: filtered})
|
|
}
|
|
func (h *KubernetesHandler) createHPAWithNS(c *gin.Context, ns string) {
|
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
|
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
|
var req k8sclient.CreateHPARequest
|
|
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, models.Response{Code: 400, Message: "参数错误"}); return }
|
|
if req.Namespace == "" { req.Namespace = ns }
|
|
client, _, err := h.getK8sClient(clusterID)
|
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
|
hpa, err := client.CreateHPA(req)
|
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "创建失败: " + err.Error()}); return }
|
|
c.JSON(200, models.Response{Code: 200, Data: hpa})
|
|
}
|
|
func (h *KubernetesHandler) deleteHPAWithNS(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.DeleteHPA(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) listCronJobsWithNS(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 }
|
|
cjs, err := client.ListCronJobs()
|
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
|
filtered := []k8sclient.CronJobItem{}
|
|
for _, cj := range cjs.Items { if cj.Metadata.Namespace == ns { filtered = append(filtered, cj) } }
|
|
c.JSON(200, models.Response{Code: 200, Data: filtered})
|
|
}
|
|
func (h *KubernetesHandler) createCronJobWithNS(c *gin.Context, ns string) {
|
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
|
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
|
var req k8sclient.CreateCronJobRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, models.Response{Code: 400, Message: "参数错误"}); return }
|
|
if req.Namespace == "" { req.Namespace = ns }
|
|
client, _, err := h.getK8sClient(clusterID)
|
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
|
cj, err := client.CreateCronJob(req)
|
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "创建失败: " + err.Error()}); return }
|
|
c.JSON(200, models.Response{Code: 200, Data: cj})
|
|
}
|
|
func (h *KubernetesHandler) suspendCronJobWithNS(c *gin.Context, ns string) {
|
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
|
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
|
var req struct { Suspend bool `json:"suspend"` }; c.ShouldBindJSON(&req)
|
|
client, _, err := h.getK8sClient(clusterID)
|
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
|
if err := client.SuspendCronJob(ns, c.Param("name"), req.Suspend); err != nil { c.JSON(500, models.Response{Code: 500, Message: "操作失败: " + err.Error()}); return }
|
|
action := "恢复"; if req.Suspend { action = "暂停" }
|
|
c.JSON(200, models.Response{Code: 200, Message: "CronJob 已" + action})
|
|
}
|
|
func (h *KubernetesHandler) triggerCronJobWithNS(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.TriggerCronJob(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) deleteCronJobWithNS(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.DeleteCronJob(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) 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, models.Response{Code: 400, Message: "参数错误"}); return }
|
|
client, _, err := h.getK8sClient(clusterID)
|
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
|
if err := client.ScaleDeployment(ns, c.Param("name"), req.Replicas); err != nil { c.JSON(500, models.Response{Code: 500, Message: "扩缩容失败: " + err.Error()}); return }
|
|
c.JSON(200, models.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, models.Response{Code: 500, Message: err.Error()}); return }
|
|
if err := client.RestartDeployment(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) 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, models.Response{Code: 400, Message: "参数错误"}); return }
|
|
client, _, err := h.getK8sClient(clusterID)
|
|
if 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)})
|
|
}
|
|
func (h *KubernetesHandler) getPodLogsWithNS(c *gin.Context, ns string) {
|
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
|
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
|
podName := c.Param("podName")
|
|
container := c.Query("container")
|
|
tailLines, _ := strconv.Atoi(c.DefaultQuery("tail", "200"))
|
|
stream := c.Query("stream") == "true"
|
|
client, _, err := h.getK8sClient(clusterID)
|
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
|
if stream {
|
|
body, err := client.GetPodLogsStream(ns, podName, container, tailLines)
|
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "获取日志流失败: " + err.Error()}); return }
|
|
defer body.Close()
|
|
c.Header("Content-Type", "text/plain; charset=utf-8")
|
|
c.Header("Cache-Control", "no-cache")
|
|
c.Stream(func(w io.Writer) bool {
|
|
buf := make([]byte, 4096); n, err := body.Read(buf)
|
|
if n > 0 { w.Write(buf[:n]); return true }
|
|
return err == nil
|
|
})
|
|
return
|
|
}
|
|
logs, err := client.GetPodLogs(ns, podName, container, tailLines)
|
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "获取日志失败: " + err.Error()}); return }
|
|
c.JSON(200, models.Response{Code: 200, Data: gin.H{"logs": logs, "pod": podName, "namespace": ns}})
|
|
}
|
|
func (h *KubernetesHandler) execInPodWithNS(c *gin.Context, ns string) {
|
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
|
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
|
podName := c.Param("podName")
|
|
var req struct { Command []string `json:"command"`; Container string `json:"container"` }
|
|
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, models.Response{Code: 400, Message: "参数错误"}); return }
|
|
client, _, err := h.getK8sClient(clusterID)
|
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
|
result, err := client.ExecInPod(ns, podName, req.Container, req.Command)
|
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "执行失败: " + err.Error()}); return }
|
|
c.JSON(200, models.Response{Code: 200, Data: result})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Helpers
|
|
// ============================================================================
|
|
|
|
func parseIntResource(val string) int {
|
|
if val == "" {
|
|
return 0
|
|
}
|
|
var i int
|
|
fmt.Sscanf(val, "%d", &i)
|
|
return i
|
|
}
|
|
|
|
func parseMemResource(val string) int64 {
|
|
if val == "" {
|
|
return 0
|
|
}
|
|
var num float64
|
|
var unit string
|
|
fmt.Sscanf(val, "%f%s", &num, &unit)
|
|
switch unit {
|
|
case "Ki":
|
|
return int64(num / 1024)
|
|
case "Mi":
|
|
return int64(num)
|
|
case "Gi":
|
|
return int64(num * 1024)
|
|
default:
|
|
return int64(num / 1024 / 1024)
|
|
}
|
|
}
|
|
|
|
|
|
// getK8sClient builds a K8s API client from cluster's stored kubeconfig
|
|
func (h *KubernetesHandler) getK8sClient(clusterID int) (*k8sclient.Client, *models.KubernetesCluster, error) {
|
|
var cluster models.KubernetesCluster
|
|
if err := h.db.First(&cluster, clusterID).Error; err != nil {
|
|
return nil, nil, fmt.Errorf("cluster not found")
|
|
}
|
|
if cluster.KubeConfig == "" {
|
|
return nil, nil, fmt.Errorf("no kubeconfig")
|
|
}
|
|
client, err := k8sclient.NewClient(cluster.KubeConfig)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return client, &cluster, nil
|
|
}
|