refactor(k8s): Q2 kubernetes.go 模块化拆分 — 4204行单文件→12个域文件
将 handlers/kubernetes.go (4204行/134函数) 按业务域拆分为: - kubernetes.go (106L): struct + constructor + 共享helpers - k8s_cluster.go (642L): 集群CRUD + 同步 + RBAC - k8s_workload.go (696L): Deployment/STS/DS/ReplicaSet操作 - k8s_pod.go (653L): Pod日志/终端/exec/指标 - k8s_pod_files.go (206L): Pod文件浏览/上传/下载 - k8s_resource.go (351L): Service/Ingress/ConfigMap/Secret/PVC/Topology - k8s_hpa_cronjob.go (384L): HPA + CronJob - k8s_yaml.go (213L): YAML get/update/delete/apply - k8s_namespace.go (305L): Namespace CRUD - k8s_events.go (207L): 事件列表+聚合 - k8s_node.go (428L): Node详情+资源分配 - k8s_compare.go (146L): 多集群对比 验收: go build零错误, 425条路由不变, 26个K8s API端点回归测试全通过
This commit is contained in:
1 parent
aaab923f4e
commit
4dd5e10b46
15 files changed
+4259
-4122
No files matched your search
@@ -122,7 +122,7 @@
|
|||||||
|
|
||||||
| 问题 | 影响 | 范围 | 建议优先级 |
|
| 问题 | 影响 | 范围 | 建议优先级 |
|
||||||
|------|------|------|:----------:|
|
|------|------|------|:----------:|
|
||||||
| `kubernetes.go` 4204 行/134 函数 | 维护困难,合并冲突高发 | 后端 | P0 |
|
| ~~`kubernetes.go` 4204 行/134 函数~~ → 已拆分为 12 个域文件(最大 696 行) | ✅ 已解决 | 后端 | ~~P0~~ done |
|
||||||
| `api.ts` 1324 行/352 方法 | 前端 API 层难以维护 | 前端 | P0 |
|
| `api.ts` 1324 行/352 方法 | 前端 API 层难以维护 | 前端 | P0 |
|
||||||
| 前端 **1026 处** `any` 类型 | 类型安全缺失,重构风险高 | 前端全局 | P0 |
|
| 前端 **1026 处** `any` 类型 | 类型安全缺失,重构风险高 | 前端全局 | P0 |
|
||||||
| K8s 双路由体系(集群域+Kuboard 域) | 路由冗余 104 条 | 后端 | P1 |
|
| K8s 双路由体系(集群域+Kuboard 域) | 路由冗余 104 条 | 后端 | P1 |
|
||||||
|
|||||||
+19
-15
@@ -42,28 +42,32 @@
|
|||||||
- [ ] ESLint `no-unused-vars` 降至 < 20 处
|
- [ ] ESLint `no-unused-vars` 降至 < 20 处
|
||||||
- [ ] `console.*` 降至 < 10 处(仅保留 error 级别)
|
- [ ] `console.*` 降至 < 10 处(仅保留 error 级别)
|
||||||
|
|
||||||
### Q2: 后端 kubernetes.go 模块化拆分 [pending]
|
### Q2: 后端 kubernetes.go 模块化拆分 [done]
|
||||||
|
|
||||||
**问题**: 单文件 4204 行 / 134 函数,全仓最大文件
|
**问题**: 单文件 4204 行 / 134 函数,全仓最大文件
|
||||||
|
|
||||||
**方案**:
|
**方案**:
|
||||||
```
|
```
|
||||||
handlers/kubernetes.go (4204L)
|
handlers/kubernetes.go (4204L → 106L core)
|
||||||
→ handlers/k8s_cluster.go (集群 CRUD + 同步 + metrics)
|
→ handlers/kubernetes.go (106L: struct + constructor + shared helpers)
|
||||||
→ handlers/k8s_workload.go (Deployment/STS/DS 读写操作)
|
→ handlers/k8s_cluster.go (642L: 集群 CRUD + 同步 + RBAC)
|
||||||
→ handlers/k8s_pod.go (Pod 日志/终端/文件/exec)
|
→ handlers/k8s_workload.go (696L: Deployment/STS/DS/ReplicaSet 读写操作)
|
||||||
→ handlers/k8s_yaml.go (通用 YAML get/update)
|
→ handlers/k8s_pod.go (653L: Pod 日志/终端/exec/指标)
|
||||||
→ handlers/k8s_namespace.go (Namespace CRUD + 配额)
|
→ handlers/k8s_pod_files.go (206L: Pod 文件浏览/上传/下载)
|
||||||
→ handlers/k8s_events.go (事件列表 + 聚合)
|
→ handlers/k8s_resource.go (351L: Service/Ingress/ConfigMap/Secret/PVC/Topology)
|
||||||
→ handlers/k8s_node.go (Node 详情 + 资源分配)
|
→ handlers/k8s_hpa_cronjob.go (384L: HPA + CronJob)
|
||||||
→ handlers/k8s_compare.go (多集群对比)
|
→ handlers/k8s_yaml.go (213L: 通用 YAML get/update/delete/apply)
|
||||||
|
→ handlers/k8s_namespace.go (305L: Namespace CRUD + 配额)
|
||||||
|
→ handlers/k8s_events.go (207L: 事件列表 + 聚合)
|
||||||
|
→ handlers/k8s_node.go (428L: Node 详情 + 资源分配)
|
||||||
|
→ handlers/k8s_compare.go (146L: 多集群对比)
|
||||||
```
|
```
|
||||||
|
|
||||||
**验收标准**:
|
**验收标准**:
|
||||||
- [ ] 拆分后单文件不超过 800 行
|
- [x] 拆分后单文件不超过 800 行(最大 k8s_workload.go 696 行)
|
||||||
- [ ] 所有路由不变,API 响应不变
|
- [x] 所有路由不变,API 响应不变(425 条路由,26 个核心端点回归测试全通过)
|
||||||
- [ ] `go build` 零错误
|
- [x] `go build` 零错误
|
||||||
- [ ] 411 条路由全部可访问(回归测试)
|
- [x] 425 条路由全部可访问(回归测试)
|
||||||
|
|
||||||
### Q3: 前端 api.ts 模块化拆分 [pending]
|
### Q3: 前端 api.ts 模块化拆分 [pending]
|
||||||
|
|
||||||
@@ -182,7 +186,7 @@ services/api.ts (1324L)
|
|||||||
| ID | 模块 | 优先级 | 状态 |
|
| ID | 模块 | 优先级 | 状态 |
|
||||||
|----|------|:------:|:----:|
|
|----|------|:------:|:----:|
|
||||||
| Q1 | 前端类型安全治理 | P0 | pending |
|
| Q1 | 前端类型安全治理 | P0 | pending |
|
||||||
| Q2 | kubernetes.go 拆分 | P0 | pending |
|
| Q2 | kubernetes.go 拆分 | P0 | **done** |
|
||||||
| Q3 | api.ts 拆分 | P0 | pending |
|
| Q3 | api.ts 拆分 | P0 | pending |
|
||||||
| F1 | 告警静默规则 | P1 | pending |
|
| F1 | 告警静默规则 | P1 | pending |
|
||||||
| F2 | 审计日志导出 | P1 | pending |
|
| F2 | 审计日志导出 | P1 | pending |
|
||||||
|
|||||||
+5
-5
@@ -153,11 +153,11 @@ docker ps --format '{{.Names}} {{.Status}}' | grep aiops-backend
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 通过标准
|
### 通过标准
|
||||||
- [ ] U1: `go build` 零错误
|
- [x] U1: `go build` 零错误
|
||||||
- [ ] U2: 每个拆分文件 < 800 行
|
- [x] U2: 每个拆分文件 < 800 行(最大 k8s_workload.go 696 行,kubernetes.go 降至 106 行核心文件)
|
||||||
- [ ] U3: 路由数量不变
|
- [x] U3: 路由数量不变(425 条路由,routes.go 未修改)
|
||||||
- [ ] U4: 7 个核心 API 端点全部返回正确数据
|
- [x] U4: 26 个核心 API 端点全部返回正确数据(集群/Deployment/Pod/Service/HPA/CronJob/PVC/STS/DS/RBAC/YAML/Events/Namespace/Compare/Node/ReplicaSet/Topology 全通过)
|
||||||
- [ ] U5: 容器 healthy
|
- [x] U5: 容器 healthy
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,642 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"ezops/internal/models"
|
||||||
|
k8sclient "ezops/pkg/kubernetesclient"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
"service_count": cluster.ServiceCount,
|
||||||
|
"deployment_count": cluster.DeploymentCount,
|
||||||
|
"cpu_usage": cluster.CPUUsage,
|
||||||
|
"memory_usage": cluster.MemoryUsage,
|
||||||
|
"labels": cluster.Labels,
|
||||||
|
"last_sync_at": cluster.LastSyncAt,
|
||||||
|
"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) 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{}
|
||||||
|
|
||||||
|
// Fetch node metrics (best-effort — metrics-server may not be installed) // Logan.Li
|
||||||
|
nodeMetrics := map[string]k8sclient.NodeMetricsItem{}
|
||||||
|
if m, err := client.GetNodeMetrics(); err == nil {
|
||||||
|
for _, item := range m.Items {
|
||||||
|
nodeMetrics[item.Metadata.Name] = item
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("[K8s] sync: GetNodeMetrics failed (metrics-server not installed?): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch pod metrics (best-effort) // Logan.Li
|
||||||
|
podMetrics := map[string]k8sclient.PodMetricsItem{} // key: "namespace/podname"
|
||||||
|
if pm, err := client.ListPodMetrics(""); err == nil {
|
||||||
|
for _, item := range pm.Items {
|
||||||
|
podMetrics[item.Metadata.Namespace+"/"+item.Metadata.Name] = item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
// Compute CPU/memory usage percentage from metrics-server data // Logan.Li
|
||||||
|
cpuUsagePct, memUsagePct := 0.0, 0.0
|
||||||
|
if m, ok := nodeMetrics[n.Metadata.Name]; ok {
|
||||||
|
cpuCores := parseIntResource(n.Status.Capacity["cpu"])
|
||||||
|
memMB := parseMemResource(n.Status.Capacity["memory"])
|
||||||
|
if cpuCores > 0 {
|
||||||
|
cpuMillicores := parseCpuNanoToMilli(m.Usage["cpu"])
|
||||||
|
cpuUsagePct = float64(cpuMillicores) / float64(cpuCores*1000) * 100
|
||||||
|
}
|
||||||
|
if memMB > 0 {
|
||||||
|
memMi := parseMemKiToMi(m.Usage["memory"])
|
||||||
|
memUsagePct = float64(memMi) / float64(memMB) * 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err2 := h.db.Exec(`INSERT INTO kubernetes_nodes (cluster_id, name, ip, role, status, os, kubelet_version, cpu, memory, max_pods, cpu_usage, memory_usage, 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"]),
|
||||||
|
cpuUsagePct, memUsagePct, 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) ListServiceAccountsNS(c *gin.Context) {
|
||||||
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
if clusterID == 0 {
|
||||||
|
c.JSON(500, models.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, models.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sas, err := client.ListServiceAccounts(c.Param("namespace"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
type SASummary struct {
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
SecretCount int `json:"secret_count"`
|
||||||
|
ImagePullSecrets []string `json:"image_pull_secrets"`
|
||||||
|
Age string `json:"age"`
|
||||||
|
}
|
||||||
|
result := make([]SASummary, 0, len(sas))
|
||||||
|
for _, sa := range sas {
|
||||||
|
ipsNames := make([]string, 0, len(sa.ImagePullSecrets))
|
||||||
|
for _, ips := range sa.ImagePullSecrets {
|
||||||
|
ipsNames = append(ipsNames, ips.Name)
|
||||||
|
}
|
||||||
|
result = append(result, SASummary{
|
||||||
|
Namespace: sa.Metadata.Namespace,
|
||||||
|
Name: sa.Metadata.Name,
|
||||||
|
SecretCount: len(sa.Secrets),
|
||||||
|
ImagePullSecrets: ipsNames,
|
||||||
|
Age: formatAge(sa.Metadata.CreationTimestamp),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListRolesNS lists Roles in a namespace
|
||||||
|
func (h *KubernetesHandler) ListRolesNS(c *gin.Context) {
|
||||||
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
if clusterID == 0 {
|
||||||
|
c.JSON(500, models.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, models.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
roles, err := client.ListRoles(c.Param("namespace"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
type RoleSummary struct {
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
RuleCount int `json:"rule_count"`
|
||||||
|
Verbs []string `json:"verbs"`
|
||||||
|
Resources []string `json:"resources"`
|
||||||
|
Age string `json:"age"`
|
||||||
|
}
|
||||||
|
result := make([]RoleSummary, 0, len(roles.Items))
|
||||||
|
for _, r := range roles.Items {
|
||||||
|
allVerbs := []string{}
|
||||||
|
allResources := []string{}
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, rule := range r.Rules {
|
||||||
|
for _, v := range rule.Verbs {
|
||||||
|
if !seen[v] {
|
||||||
|
allVerbs = append(allVerbs, v)
|
||||||
|
seen[v] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, res := range rule.Resources {
|
||||||
|
if !seen[res] {
|
||||||
|
allResources = append(allResources, res)
|
||||||
|
seen[res] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, RoleSummary{
|
||||||
|
Namespace: r.Metadata.Namespace,
|
||||||
|
Name: r.Metadata.Name,
|
||||||
|
RuleCount: len(r.Rules),
|
||||||
|
Verbs: allVerbs,
|
||||||
|
Resources: allResources,
|
||||||
|
Age: formatAge(r.Metadata.CreationTimestamp),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListRoleBindingsNS lists RoleBindings in a namespace
|
||||||
|
func (h *KubernetesHandler) ListRoleBindingsNS(c *gin.Context) {
|
||||||
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
if clusterID == 0 {
|
||||||
|
c.JSON(500, models.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, models.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rbs, err := client.ListRoleBindings(c.Param("namespace"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
type RBSummary struct {
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
RoleRefKind string `json:"role_ref_kind"`
|
||||||
|
RoleRefName string `json:"role_ref_name"`
|
||||||
|
Subjects []string `json:"subjects"`
|
||||||
|
Age string `json:"age"`
|
||||||
|
}
|
||||||
|
result := make([]RBSummary, 0, len(rbs.Items))
|
||||||
|
for _, rb := range rbs.Items {
|
||||||
|
subjStrs := make([]string, 0, len(rb.Subjects))
|
||||||
|
for _, s := range rb.Subjects {
|
||||||
|
if s.Namespace != "" && s.Namespace != rb.Metadata.Namespace {
|
||||||
|
subjStrs = append(subjStrs, fmt.Sprintf("%s/%s (%s)", s.Kind, s.Name, s.Namespace))
|
||||||
|
} else {
|
||||||
|
subjStrs = append(subjStrs, fmt.Sprintf("%s/%s", s.Kind, s.Name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, RBSummary{
|
||||||
|
Namespace: rb.Metadata.Namespace,
|
||||||
|
Name: rb.Metadata.Name,
|
||||||
|
RoleRefKind: rb.RoleRef.Kind,
|
||||||
|
RoleRefName: rb.RoleRef.Name,
|
||||||
|
Subjects: subjStrs,
|
||||||
|
Age: formatAge(rb.Metadata.CreationTimestamp),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"ezops/internal/models"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *KubernetesHandler) GetMultiClusterComparison(c *gin.Context) {
|
||||||
|
// 1. 获取所有集群
|
||||||
|
var clusters []models.KubernetesCluster
|
||||||
|
h.db.Find(&clusters)
|
||||||
|
|
||||||
|
type ClusterComparison struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
NodeCount int `json:"node_count"`
|
||||||
|
PodCount int `json:"pod_count"`
|
||||||
|
DeploymentCount int `json:"deployment_count"`
|
||||||
|
ServiceCount int `json:"service_count"`
|
||||||
|
CPUUsage float64 `json:"cpu_usage"`
|
||||||
|
MemoryUsage float64 `json:"memory_usage"`
|
||||||
|
APIServer string `json:"api_server"`
|
||||||
|
LastSyncAt *time.Time `json:"last_sync_at"`
|
||||||
|
|
||||||
|
// Node-level aggregated data from kubernetes_nodes table
|
||||||
|
TotalCPUCores int `json:"total_cpu_cores"`
|
||||||
|
TotalMemoryMB int64 `json:"total_memory_mb"`
|
||||||
|
MasterCount int `json:"master_count"`
|
||||||
|
WorkerCount int `json:"worker_count"`
|
||||||
|
HealthyNodes int `json:"healthy_nodes"`
|
||||||
|
UnhealthyNodes int `json:"unhealthy_nodes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 获取所有节点的聚合数据(按 cluster_id 分组)
|
||||||
|
type NodeAgg struct {
|
||||||
|
ClusterID uint
|
||||||
|
NodeCount int
|
||||||
|
MasterCount int
|
||||||
|
WorkerCount int
|
||||||
|
HealthyNodes int
|
||||||
|
UnhealthyNodes int
|
||||||
|
TotalCPU int
|
||||||
|
TotalMemory int64
|
||||||
|
AvgCPUUsage float64
|
||||||
|
AvgMemUsage float64
|
||||||
|
}
|
||||||
|
nodeAggMap := map[uint]*NodeAgg{}
|
||||||
|
{
|
||||||
|
var nodes []models.KubernetesNode
|
||||||
|
h.db.Find(&nodes)
|
||||||
|
for _, n := range nodes {
|
||||||
|
agg, ok := nodeAggMap[n.ClusterID]
|
||||||
|
if !ok {
|
||||||
|
agg = &NodeAgg{ClusterID: n.ClusterID}
|
||||||
|
nodeAggMap[n.ClusterID] = agg
|
||||||
|
}
|
||||||
|
agg.NodeCount++
|
||||||
|
agg.TotalCPU += n.CPU
|
||||||
|
agg.TotalMemory += n.Memory
|
||||||
|
if n.CPUUsage > 0 || n.MemoryUsage > 0 {
|
||||||
|
if agg.AvgCPUUsage == 0 { agg.AvgCPUUsage = n.CPUUsage } else { agg.AvgCPUUsage = (agg.AvgCPUUsage*float64(agg.NodeCount-1) + n.CPUUsage) / float64(agg.NodeCount) }
|
||||||
|
if agg.AvgMemUsage == 0 { agg.AvgMemUsage = n.MemoryUsage } else { agg.AvgMemUsage = (agg.AvgMemUsage*float64(agg.NodeCount-1) + n.MemoryUsage) / float64(agg.NodeCount) }
|
||||||
|
}
|
||||||
|
if n.Role == "master" {
|
||||||
|
agg.MasterCount++
|
||||||
|
} else {
|
||||||
|
agg.WorkerCount++
|
||||||
|
}
|
||||||
|
if n.Status == "Ready" {
|
||||||
|
agg.HealthyNodes++
|
||||||
|
} else {
|
||||||
|
agg.UnhealthyNodes++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 构建对比数据
|
||||||
|
comparisons := []ClusterComparison{}
|
||||||
|
totalNodes, totalPods, totalDeployments, totalServices := 0, 0, 0, 0
|
||||||
|
for _, cl := range clusters {
|
||||||
|
cc := ClusterComparison{
|
||||||
|
ID: cl.ID,
|
||||||
|
Name: cl.Name,
|
||||||
|
Code: cl.Code,
|
||||||
|
Status: cl.Status,
|
||||||
|
Version: cl.Version,
|
||||||
|
NodeCount: cl.NodeCount,
|
||||||
|
PodCount: cl.PodCount,
|
||||||
|
DeploymentCount: cl.DeploymentCount,
|
||||||
|
ServiceCount: cl.ServiceCount,
|
||||||
|
CPUUsage: cl.CPUUsage,
|
||||||
|
MemoryUsage: cl.MemoryUsage,
|
||||||
|
APIServer: cl.APIServer,
|
||||||
|
LastSyncAt: cl.LastSyncAt,
|
||||||
|
}
|
||||||
|
// 优先用 node 级别的实时聚合数据
|
||||||
|
if agg, ok := nodeAggMap[cl.ID]; ok && agg.NodeCount > 0 {
|
||||||
|
cc.TotalCPUCores = agg.TotalCPU
|
||||||
|
cc.TotalMemoryMB = agg.TotalMemory
|
||||||
|
cc.MasterCount = agg.MasterCount
|
||||||
|
cc.WorkerCount = agg.WorkerCount
|
||||||
|
cc.HealthyNodes = agg.HealthyNodes
|
||||||
|
cc.UnhealthyNodes = agg.UnhealthyNodes
|
||||||
|
// 如果集群级别的 cpu_usage 为 0,使用节点平均值
|
||||||
|
if cc.CPUUsage == 0 && agg.AvgCPUUsage > 0 {
|
||||||
|
cc.CPUUsage = agg.AvgCPUUsage
|
||||||
|
}
|
||||||
|
if cc.MemoryUsage == 0 && agg.AvgMemUsage > 0 {
|
||||||
|
cc.MemoryUsage = agg.AvgMemUsage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
totalNodes += cc.NodeCount
|
||||||
|
totalPods += cc.PodCount
|
||||||
|
totalDeployments += cc.DeploymentCount
|
||||||
|
totalServices += cc.ServiceCount
|
||||||
|
comparisons = append(comparisons, cc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 返回结果
|
||||||
|
c.JSON(200, models.Response{
|
||||||
|
Code: 200,
|
||||||
|
Message: "success",
|
||||||
|
Data: gin.H{
|
||||||
|
"clusters": comparisons,
|
||||||
|
"totals": gin.H{
|
||||||
|
"cluster_count": len(clusters),
|
||||||
|
"node_count": totalNodes,
|
||||||
|
"pod_count": totalPods,
|
||||||
|
"deployment_count": totalDeployments,
|
||||||
|
"service_count": totalServices,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// RBAC — ServiceAccount / Role / RoleBinding (code-based)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// ListServiceAccountsNS lists ServiceAccounts in a namespace
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"ezops/internal/models"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *KubernetesHandler) ListEvents(c *gin.Context) {
|
||||||
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
|
||||||
|
ns := c.Query("namespace")
|
||||||
|
eventType := c.Query("type") // Normal or Warning
|
||||||
|
events, err := client.ListEvents(ns)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||||
|
|
||||||
|
type EventSummary struct {
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Object string `json:"object"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
FirstTime string `json:"first_time"`
|
||||||
|
LastTime string `json:"last_time"`
|
||||||
|
}
|
||||||
|
result := []EventSummary{}
|
||||||
|
for _, e := range events.Items {
|
||||||
|
if eventType != "" && e.Type != eventType { continue }
|
||||||
|
object := fmt.Sprintf("%s/%s", e.InvolvedObject.Kind, e.InvolvedObject.Name)
|
||||||
|
if e.InvolvedObject.Namespace != "" {
|
||||||
|
object = fmt.Sprintf("%s/%s/%s", e.InvolvedObject.Namespace, e.InvolvedObject.Kind, e.InvolvedObject.Name)
|
||||||
|
}
|
||||||
|
firstTime := ""; if !e.FirstTimestamp.IsZero() { firstTime = e.FirstTimestamp.Format(time.RFC3339) }
|
||||||
|
lastTime := ""; if !e.LastTimestamp.IsZero() { lastTime = e.LastTimestamp.Format(time.RFC3339) }
|
||||||
|
result = append(result, EventSummary{
|
||||||
|
Namespace: e.Metadata.Namespace, Name: e.Metadata.Name, Type: e.Type, Reason: e.Reason,
|
||||||
|
Message: e.Message, Object: object, Count: int(e.Count),
|
||||||
|
FirstTime: firstTime, LastTime: lastTime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Event Aggregation — 事件聚合统计 (M12)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// GetEventsSummary returns aggregated event statistics for dashboard
|
||||||
|
func (h *KubernetesHandler) GetEventsSummary(c *gin.Context) {
|
||||||
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
if clusterID == 0 {
|
||||||
|
c.JSON(500, models.Response{Code: 500, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, models.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ns := c.Query("namespace")
|
||||||
|
events, err := client.ListEvents(ns)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time window filter (hours)
|
||||||
|
timeWindowHours := 0
|
||||||
|
if tw := c.Query("hours"); tw != "" {
|
||||||
|
if v, err := strconv.Atoi(tw); err == nil && v > 0 {
|
||||||
|
timeWindowHours = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
cutoff := time.Time{}
|
||||||
|
if timeWindowHours > 0 {
|
||||||
|
cutoff = now.Add(-time.Duration(timeWindowHours) * time.Hour)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregation maps
|
||||||
|
totalCount := 0
|
||||||
|
warningCount := 0
|
||||||
|
normalCount := 0
|
||||||
|
byReason := map[string]int{}
|
||||||
|
byNamespace := map[string]int{}
|
||||||
|
byKind := map[string]int{}
|
||||||
|
recentWarnings := []map[string]interface{}{}
|
||||||
|
|
||||||
|
for _, e := range events.Items {
|
||||||
|
// Time window filter
|
||||||
|
if !cutoff.IsZero() && !e.LastTimestamp.IsZero() && e.LastTimestamp.Before(cutoff) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
totalCount++
|
||||||
|
count := int(e.Count)
|
||||||
|
if count == 0 {
|
||||||
|
count = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
nsName := e.Metadata.Namespace
|
||||||
|
if nsName == "" {
|
||||||
|
nsName = e.InvolvedObject.Namespace
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Type == "Warning" {
|
||||||
|
warningCount += count
|
||||||
|
byReason[e.Reason] += count
|
||||||
|
byNamespace[nsName] += count
|
||||||
|
byKind[e.InvolvedObject.Kind] += count
|
||||||
|
|
||||||
|
// Collect recent warning events (top 10 by lastTimestamp)
|
||||||
|
if len(recentWarnings) < 50 {
|
||||||
|
recentWarnings = append(recentWarnings, map[string]interface{}{
|
||||||
|
"namespace": nsName,
|
||||||
|
"type": e.Type,
|
||||||
|
"reason": e.Reason,
|
||||||
|
"message": e.Message,
|
||||||
|
"object": fmt.Sprintf("%s/%s", e.InvolvedObject.Kind, e.InvolvedObject.Name),
|
||||||
|
"count": count,
|
||||||
|
"last_time": e.LastTimestamp.Format(time.RFC3339),
|
||||||
|
"first_time": e.FirstTimestamp.Format(time.RFC3339),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
normalCount += count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort byReason by count desc → top reasons
|
||||||
|
type ReasonStat struct {
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
reasonList := make([]ReasonStat, 0, len(byReason))
|
||||||
|
for r, cnt := range byReason {
|
||||||
|
reasonList = append(reasonList, ReasonStat{Reason: r, Count: cnt})
|
||||||
|
}
|
||||||
|
sort.Slice(reasonList, func(i, j int) bool { return reasonList[i].Count > reasonList[j].Count })
|
||||||
|
if len(reasonList) > 15 {
|
||||||
|
reasonList = reasonList[:15]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort byNamespace
|
||||||
|
type NsStat struct {
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
nsList := make([]NsStat, 0, len(byNamespace))
|
||||||
|
for n, cnt := range byNamespace {
|
||||||
|
nsList = append(nsList, NsStat{Namespace: n, Count: cnt})
|
||||||
|
}
|
||||||
|
sort.Slice(nsList, func(i, j int) bool { return nsList[i].Count > nsList[j].Count })
|
||||||
|
if len(nsList) > 15 {
|
||||||
|
nsList = nsList[:15]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort byKind
|
||||||
|
type KindStat struct {
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
kindList := make([]KindStat, 0, len(byKind))
|
||||||
|
for k, cnt := range byKind {
|
||||||
|
kindList = append(kindList, KindStat{Kind: k, Count: cnt})
|
||||||
|
}
|
||||||
|
sort.Slice(kindList, func(i, j int) bool { return kindList[i].Count > kindList[j].Count })
|
||||||
|
|
||||||
|
// Sort recentWarnings by last_time desc
|
||||||
|
sort.Slice(recentWarnings, func(i, j int) bool {
|
||||||
|
ti, _ := recentWarnings[i]["last_time"].(string)
|
||||||
|
tj, _ := recentWarnings[j]["last_time"].(string)
|
||||||
|
return ti > tj
|
||||||
|
})
|
||||||
|
if len(recentWarnings) > 10 {
|
||||||
|
recentWarnings = recentWarnings[:10]
|
||||||
|
}
|
||||||
|
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"total": totalCount,
|
||||||
|
"warning_count": warningCount,
|
||||||
|
"normal_count": normalCount,
|
||||||
|
"by_reason": reasonList,
|
||||||
|
"by_namespace": nsList,
|
||||||
|
"by_kind": kindList,
|
||||||
|
"recent_warnings": recentWarnings,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Capacity Analysis — CPU/Memory request/limit per Deployment
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// GetCapacityAnalysis returns resource analysis for all deployments
|
||||||
@@ -0,0 +1,384 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"ezops/internal/models"
|
||||||
|
k8sclient "ezops/pkg/kubernetesclient"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
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) 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) 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 }
|
||||||
|
type HPASummary struct {
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
TargetKind string `json:"target_kind"`
|
||||||
|
TargetName string `json:"target_name"`
|
||||||
|
MinReplicas int `json:"min_replicas"`
|
||||||
|
MaxReplicas int `json:"max_replicas"`
|
||||||
|
CPUPercent int `json:"cpu_percent"`
|
||||||
|
MemPercent int `json:"mem_percent"`
|
||||||
|
CurrentReplicas int `json:"current_replicas"`
|
||||||
|
}
|
||||||
|
result := []HPASummary{}
|
||||||
|
for _, h := range hpas.Items {
|
||||||
|
if h.Metadata.Namespace != ns { continue }
|
||||||
|
cpu := 0; mem := 0
|
||||||
|
for _, m := range h.Spec.Metrics {
|
||||||
|
if m.Resource != nil {
|
||||||
|
if m.Resource.Name == "cpu" { cpu = m.Resource.Target.AverageUtilization }
|
||||||
|
if m.Resource.Name == "memory" { mem = m.Resource.Target.AverageUtilization }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, HPASummary{
|
||||||
|
Namespace: h.Metadata.Namespace,
|
||||||
|
Name: h.Metadata.Name,
|
||||||
|
TargetKind: h.Spec.ScaleTargetRef.Kind,
|
||||||
|
TargetName: h.Spec.ScaleTargetRef.Name,
|
||||||
|
MinReplicas: h.Spec.MinReplicas,
|
||||||
|
MaxReplicas: h.Spec.MaxReplicas,
|
||||||
|
CPUPercent: cpu,
|
||||||
|
MemPercent: mem,
|
||||||
|
CurrentReplicas: h.Status.CurrentReplicas,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||||
|
}
|
||||||
|
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) ListHPAsAllNS(c *gin.Context) {
|
||||||
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
hpas, err := client.ListHPAs()
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||||
|
type HPASummary struct {
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
TargetKind string `json:"target_kind"`
|
||||||
|
TargetName string `json:"target_name"`
|
||||||
|
MinReplicas int `json:"min_replicas"`
|
||||||
|
MaxReplicas int `json:"max_replicas"`
|
||||||
|
CPUPercent int `json:"cpu_percent"`
|
||||||
|
MemPercent int `json:"mem_percent"`
|
||||||
|
CurrentReplicas int `json:"current_replicas"`
|
||||||
|
}
|
||||||
|
result := []HPASummary{}
|
||||||
|
for _, h := range hpas.Items {
|
||||||
|
cpu := 0; mem := 0
|
||||||
|
for _, m := range h.Spec.Metrics {
|
||||||
|
if m.Resource != nil {
|
||||||
|
if m.Resource.Name == "cpu" { cpu = m.Resource.Target.AverageUtilization }
|
||||||
|
if m.Resource.Name == "memory" { mem = m.Resource.Target.AverageUtilization }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, HPASummary{
|
||||||
|
Namespace: h.Metadata.Namespace,
|
||||||
|
Name: h.Metadata.Name,
|
||||||
|
TargetKind: h.Spec.ScaleTargetRef.Kind,
|
||||||
|
TargetName: h.Spec.ScaleTargetRef.Name,
|
||||||
|
MinReplicas: h.Spec.MinReplicas,
|
||||||
|
MaxReplicas: h.Spec.MaxReplicas,
|
||||||
|
CPUPercent: cpu,
|
||||||
|
MemPercent: mem,
|
||||||
|
CurrentReplicas: h.Status.CurrentReplicas,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Helpers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// buildPodMetricsMap fetches pod metrics and returns a map keyed by "namespace/podname" → aggregated CPU/Memory strings.
|
||||||
|
// Best-effort: returns empty map if metrics-server is unavailable. // Logan.Li M2
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"ezops/internal/models"
|
||||||
|
k8sclient "ezops/pkg/kubernetesclient"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
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})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Namespace Management (M14 — 资源配额 + CRUD)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// NamespaceDetail is the enriched namespace info for the management page
|
||||||
|
type NamespaceDetail struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Age string `json:"age"`
|
||||||
|
Labels map[string]string `json:"labels"`
|
||||||
|
Annotations map[string]string `json:"annotations,omitempty"`
|
||||||
|
ResourceCounts map[string]int `json:"resource_counts"`
|
||||||
|
HasResourceQuota bool `json:"has_resource_quota"`
|
||||||
|
HasLimitRange bool `json:"has_limit_range"`
|
||||||
|
ResourceQuotas []k8sclient.ResourceQuotaItem `json:"resource_quotas,omitempty"`
|
||||||
|
LimitRanges []k8sclient.LimitRangeItem `json:"limit_ranges,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNamespacesDetail returns all namespaces with metadata, resource counts and quota info
|
||||||
|
func (h *KubernetesHandler) GetNamespacesDetail(c *gin.Context) {
|
||||||
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
|
if clusterID == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "cluster not specified"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nsList, err := client.ListNamespacesDetailed()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Efficient: fetch all resources once, group by namespace
|
||||||
|
depCounts := map[string]int{}
|
||||||
|
if deps, err := client.ListDeployments(); err == nil {
|
||||||
|
for _, d := range deps.Items {
|
||||||
|
depCounts[d.Metadata.Namespace]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
podCounts := map[string]int{}
|
||||||
|
if pods, err := client.ListPods(); err == nil {
|
||||||
|
for _, p := range pods.Items {
|
||||||
|
podCounts[p.Metadata.Namespace]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
svcCounts := map[string]int{}
|
||||||
|
if svcs, err := client.ListServices(); err == nil {
|
||||||
|
for _, s := range svcs.Items {
|
||||||
|
svcCounts[s.Metadata.Namespace]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// All-namespace ResourceQuotas
|
||||||
|
quotaByNS := map[string][]k8sclient.ResourceQuotaItem{}
|
||||||
|
if quotas, err := client.ListResourceQuotas(""); err == nil {
|
||||||
|
for _, q := range quotas {
|
||||||
|
quotaByNS[q.Metadata.Namespace] = append(quotaByNS[q.Metadata.Namespace], q)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// All-namespace LimitRanges
|
||||||
|
lrByNS := map[string][]k8sclient.LimitRangeItem{}
|
||||||
|
if lrs, err := client.ListLimitRanges(""); err == nil {
|
||||||
|
for _, lr := range lrs {
|
||||||
|
lrByNS[lr.Metadata.Namespace] = append(lrByNS[lr.Metadata.Namespace], lr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]NamespaceDetail, 0, len(nsList.Items))
|
||||||
|
for _, ns := range nsList.Items {
|
||||||
|
name := ns.Metadata.Name
|
||||||
|
nd := NamespaceDetail{
|
||||||
|
Name: name,
|
||||||
|
Status: ns.Status.Phase,
|
||||||
|
Age: formatAge(ns.Metadata.CreationTimestamp),
|
||||||
|
Labels: ns.Metadata.Labels,
|
||||||
|
Annotations: ns.Metadata.Annotations,
|
||||||
|
ResourceCounts: map[string]int{
|
||||||
|
"deployments": depCounts[name],
|
||||||
|
"pods": podCounts[name],
|
||||||
|
"services": svcCounts[name],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if nd.Labels == nil {
|
||||||
|
nd.Labels = map[string]string{}
|
||||||
|
}
|
||||||
|
if qs, ok := quotaByNS[name]; ok && len(qs) > 0 {
|
||||||
|
nd.HasResourceQuota = true
|
||||||
|
nd.ResourceQuotas = qs
|
||||||
|
}
|
||||||
|
if lrs, ok := lrByNS[name]; ok && len(lrs) > 0 {
|
||||||
|
nd.HasLimitRange = true
|
||||||
|
nd.LimitRanges = lrs
|
||||||
|
}
|
||||||
|
result = append(result, nd)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: result})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNamespaceDetailHandler returns a single namespace detail with quota info
|
||||||
|
func (h *KubernetesHandler) GetNamespaceDetailHandler(c *gin.Context) {
|
||||||
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
|
if clusterID == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "cluster not specified"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nsName := c.Param("namespace")
|
||||||
|
if nsName == "" {
|
||||||
|
nsName = c.Param("name")
|
||||||
|
}
|
||||||
|
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ns, err := client.GetNamespace(nsName)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nd := NamespaceDetail{
|
||||||
|
Name: ns.Metadata.Name,
|
||||||
|
Status: ns.Status.Phase,
|
||||||
|
Age: formatAge(ns.Metadata.CreationTimestamp),
|
||||||
|
Labels: ns.Metadata.Labels,
|
||||||
|
Annotations: ns.Metadata.Annotations,
|
||||||
|
ResourceCounts: map[string]int{},
|
||||||
|
}
|
||||||
|
if nd.Labels == nil {
|
||||||
|
nd.Labels = map[string]string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch resource counts (single namespace)
|
||||||
|
if deps, err := client.ListDeployments(); err == nil {
|
||||||
|
count := 0
|
||||||
|
for _, d := range deps.Items {
|
||||||
|
if d.Metadata.Namespace == nsName { count++ }
|
||||||
|
}
|
||||||
|
nd.ResourceCounts["deployments"] = count
|
||||||
|
}
|
||||||
|
if pods, err := client.ListPods(); err == nil {
|
||||||
|
count := 0
|
||||||
|
for _, p := range pods.Items {
|
||||||
|
if p.Metadata.Namespace == nsName { count++ }
|
||||||
|
}
|
||||||
|
nd.ResourceCounts["pods"] = count
|
||||||
|
}
|
||||||
|
if svcs, err := client.ListServices(); err == nil {
|
||||||
|
count := 0
|
||||||
|
for _, s := range svcs.Items {
|
||||||
|
if s.Metadata.Namespace == nsName { count++ }
|
||||||
|
}
|
||||||
|
nd.ResourceCounts["services"] = count
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch quotas + limit ranges for this namespace
|
||||||
|
if quotas, err := client.ListResourceQuotas(nsName); err == nil && len(quotas) > 0 {
|
||||||
|
nd.HasResourceQuota = true
|
||||||
|
nd.ResourceQuotas = quotas
|
||||||
|
}
|
||||||
|
if lrs, err := client.ListLimitRanges(nsName); err == nil && len(lrs) > 0 {
|
||||||
|
nd.HasLimitRange = true
|
||||||
|
nd.LimitRanges = lrs
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Data: nd})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateNamespaceHandler creates a new namespace
|
||||||
|
func (h *KubernetesHandler) CreateNamespaceHandler(c *gin.Context) {
|
||||||
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
|
if clusterID == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "cluster not specified"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
Labels map[string]string `json:"labels"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "name is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety: block critical system namespaces
|
||||||
|
systemNS := map[string]bool{
|
||||||
|
"kube-system": true, "kube-public": true, "kube-node-lease": true,
|
||||||
|
"default": true, "kubernetes-dashboard": true,
|
||||||
|
}
|
||||||
|
if systemNS[req.Name] {
|
||||||
|
c.JSON(http.StatusForbidden, models.Response{Code: 403, Message: "cannot create system namespace: " + req.Name})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ns, err := client.CreateNamespace(req.Name, req.Labels)
|
||||||
|
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: ns, Message: "命名空间创建成功"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteNamespaceHandler deletes a namespace
|
||||||
|
func (h *KubernetesHandler) DeleteNamespaceHandler(c *gin.Context) {
|
||||||
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
|
if clusterID == 0 {
|
||||||
|
clusterID = h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
}
|
||||||
|
if clusterID == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "cluster not specified"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nsName := c.Param("namespace")
|
||||||
|
if nsName == "" {
|
||||||
|
nsName = c.Param("name")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety: block deletion of critical system namespaces
|
||||||
|
systemNS := map[string]bool{
|
||||||
|
"kube-system": true, "kube-public": true, "kube-node-lease": true,
|
||||||
|
"default": true, "devops": true,
|
||||||
|
}
|
||||||
|
if systemNS[nsName] {
|
||||||
|
c.JSON(http.StatusForbidden, models.Response{Code: 403, Message: "禁止删除系统命名空间: " + nsName})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.DeleteNamespace(nsName); 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, Message: "命名空间已删除"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ConfigMap & Secret (Kuboard-style namespace resources)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// ListConfigMaps 获取命名空间下的 ConfigMap 列表
|
||||||
@@ -0,0 +1,428 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"ezops/internal/models"
|
||||||
|
k8sclient "ezops/pkg/kubernetesclient"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to fetch metrics (best-effort, metrics-server may not be installed)
|
||||||
|
metrics := map[string]k8sclient.NodeMetricsItem{}
|
||||||
|
if m, err := client.GetNodeMetrics(); err == nil {
|
||||||
|
for _, item := range m.Items {
|
||||||
|
metrics[item.Metadata.Name] = item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
// Metrics (optional, omitted if empty)
|
||||||
|
CPUUsageM string `json:"cpu_usage_m,omitempty"` // e.g. "122m"
|
||||||
|
MemUsageMi string `json:"mem_usage_mi,omitempty"` // e.g. "2105Mi"
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s := 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"]),
|
||||||
|
}
|
||||||
|
if m, ok := metrics[n.Metadata.Name]; ok {
|
||||||
|
// K8s metrics API returns CPU in nanocores ("80410173n"), memory in Ki ("2154292Ki")
|
||||||
|
s.CPUUsageM = formatCpuFromNano(m.Usage["cpu"])
|
||||||
|
s.MemUsageMi = formatMemFromKi(m.Usage["memory"])
|
||||||
|
}
|
||||||
|
result = append(result, s)
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: result})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNodeDetail 获取节点详情(PID/容器运行时/分配率)// M10
|
||||||
|
// @Summary 获取节点详情
|
||||||
|
// @Description 获取指定集群中单个节点的详细信息,包含系统信息、资源分配、污点、条件等
|
||||||
|
// @Tags K8s集群
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Router /kubernetes/{clusterCode}/nodes/{name} [get]
|
||||||
|
func (h *KubernetesHandler) GetNodeDetail(c *gin.Context) {
|
||||||
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
|
if clusterID == 0 {
|
||||||
|
code := c.Param("clusterCode")
|
||||||
|
if code == "" {
|
||||||
|
code = c.Param("id")
|
||||||
|
}
|
||||||
|
clusterID = h.resolveClusterCode(code)
|
||||||
|
}
|
||||||
|
if clusterID == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "cluster not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeName := c.Param("name")
|
||||||
|
if nodeName == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "node name required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Get node detail
|
||||||
|
node, err := client.GetNode(nodeName)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s API error: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Get node metrics (best-effort)
|
||||||
|
var cpuUsageM, memUsageMi string
|
||||||
|
if m, err := client.GetNodeMetrics(); err == nil {
|
||||||
|
for _, item := range m.Items {
|
||||||
|
if item.Metadata.Name == nodeName {
|
||||||
|
cpuUsageM = formatCpuFromNano(item.Usage["cpu"])
|
||||||
|
memUsageMi = formatMemFromKi(item.Usage["memory"])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Get pods on this node for allocation calculation
|
||||||
|
pods, _ := client.ListPods()
|
||||||
|
podCount := 0
|
||||||
|
type podBrief struct {
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
IP string `json:"ip"`
|
||||||
|
CPURequestM int `json:"cpu_request_m"`
|
||||||
|
MemRequestMi int64 `json:"mem_request_mi"`
|
||||||
|
RestartCount int `json:"restart_count"`
|
||||||
|
}
|
||||||
|
var podList []podBrief
|
||||||
|
totalCPURequestM := 0
|
||||||
|
totalMemRequestMi := int64(0)
|
||||||
|
for _, p := range pods.Items {
|
||||||
|
if p.Spec.NodeName != nodeName {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
podCount++
|
||||||
|
podCPUReq := 0
|
||||||
|
podMemReq := int64(0)
|
||||||
|
for _, ctr := range p.Spec.Containers {
|
||||||
|
if req := ctr.Resources.Requests; req != nil {
|
||||||
|
podCPUReq += parseCpuToMilli(req["cpu"])
|
||||||
|
podMemReq += parseMemToMi(req["memory"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
totalCPURequestM += podCPUReq
|
||||||
|
totalMemRequestMi += podMemReq
|
||||||
|
|
||||||
|
status := "Unknown"
|
||||||
|
if p.Status.Phase != "" {
|
||||||
|
status = p.Status.Phase
|
||||||
|
}
|
||||||
|
restartCount := 0
|
||||||
|
for _, cs := range p.Status.ContainerStatuses {
|
||||||
|
restartCount += cs.RestartCount
|
||||||
|
}
|
||||||
|
podList = append(podList, podBrief{
|
||||||
|
Namespace: p.Metadata.Namespace,
|
||||||
|
Name: p.Metadata.Name,
|
||||||
|
Status: status,
|
||||||
|
IP: p.Status.PodIP,
|
||||||
|
CPURequestM: podCPUReq,
|
||||||
|
MemRequestMi: podMemReq,
|
||||||
|
RestartCount: restartCount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Determine role
|
||||||
|
role := "worker"
|
||||||
|
for k := range node.Metadata.Labels {
|
||||||
|
if k == "node-role.kubernetes.io/master" || k == "node-role.kubernetes.io/control-plane" {
|
||||||
|
role = "master"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Determine status
|
||||||
|
nodeStatus := "NotReady"
|
||||||
|
for _, cond := range node.Status.Conditions {
|
||||||
|
if cond.Type == "Ready" && cond.Status == "True" {
|
||||||
|
nodeStatus = "Ready"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Get IP
|
||||||
|
internalIP := ""
|
||||||
|
externalIP := ""
|
||||||
|
hostname := ""
|
||||||
|
for _, addr := range node.Status.Addresses {
|
||||||
|
switch addr.Type {
|
||||||
|
case "InternalIP":
|
||||||
|
internalIP = addr.Address
|
||||||
|
case "ExternalIP":
|
||||||
|
externalIP = addr.Address
|
||||||
|
case "Hostname":
|
||||||
|
hostname = addr.Address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Build conditions list
|
||||||
|
type condInfo struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
conditions := []condInfo{}
|
||||||
|
for _, cond := range node.Status.Conditions {
|
||||||
|
conditions = append(conditions, condInfo{
|
||||||
|
Type: cond.Type,
|
||||||
|
Status: cond.Status,
|
||||||
|
Reason: cond.Reason,
|
||||||
|
Message: cond.Message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Build taints list
|
||||||
|
type taintInfo struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Effect string `json:"effect"`
|
||||||
|
}
|
||||||
|
taints := []taintInfo{}
|
||||||
|
for _, t := range node.Spec.Taints {
|
||||||
|
taints = append(taints, taintInfo{
|
||||||
|
Key: t.Key,
|
||||||
|
Value: t.Value,
|
||||||
|
Effect: t.Effect,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Calculate allocation rate
|
||||||
|
capCPU := parseIntResource(node.Status.Capacity["cpu"])
|
||||||
|
allocCPU := parseIntResource(node.Status.Allocatable["cpu"])
|
||||||
|
capMem := parseMemResource(node.Status.Capacity["memory"])
|
||||||
|
allocMem := parseMemResource(node.Status.Allocatable["memory"])
|
||||||
|
capPods := parseIntResource(node.Status.Capacity["pods"])
|
||||||
|
allocPods := parseIntResource(node.Status.Allocatable["pods"])
|
||||||
|
|
||||||
|
cpuReqCores := float64(totalCPURequestM) / 1000.0
|
||||||
|
cpuAllocPct := 0.0
|
||||||
|
if allocCPU > 0 {
|
||||||
|
cpuAllocPct = (cpuReqCores / float64(allocCPU)) * 100
|
||||||
|
}
|
||||||
|
memAllocPct := 0.0
|
||||||
|
if allocMem > 0 {
|
||||||
|
memAllocPct = (float64(totalMemRequestMi) / float64(allocMem)) * 100
|
||||||
|
}
|
||||||
|
podAllocPct := 0.0
|
||||||
|
if allocPods > 0 {
|
||||||
|
podAllocPct = (float64(podCount) / float64(allocPods)) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. Build labels list
|
||||||
|
type labelEntry struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
labels := []labelEntry{}
|
||||||
|
for k, v := range node.Metadata.Labels {
|
||||||
|
labels = append(labels, labelEntry{Key: k, Value: v})
|
||||||
|
}
|
||||||
|
|
||||||
|
detail := map[string]interface{}{
|
||||||
|
"name": node.Metadata.Name,
|
||||||
|
"role": role,
|
||||||
|
"status": nodeStatus,
|
||||||
|
"creation_time": node.Metadata.CreationTimestamp,
|
||||||
|
"internal_ip": internalIP,
|
||||||
|
"external_ip": externalIP,
|
||||||
|
"hostname": hostname,
|
||||||
|
"pod_cidr": node.Spec.PodCIDR,
|
||||||
|
"container_runtime": node.Status.NodeInfo.ContainerRuntimeVersion,
|
||||||
|
"kubelet_version": node.Status.NodeInfo.KubeletVersion,
|
||||||
|
"os_image": node.Status.NodeInfo.OSImage,
|
||||||
|
"kernel_version": node.Status.NodeInfo.KernelVersion,
|
||||||
|
"architecture": node.Status.NodeInfo.Architecture,
|
||||||
|
"operating_system": node.Status.NodeInfo.OperatingSystem,
|
||||||
|
// Capacity & Allocatable
|
||||||
|
"capacity": node.Status.Capacity,
|
||||||
|
"allocatable": node.Status.Allocatable,
|
||||||
|
// Metrics
|
||||||
|
"cpu_usage_m": cpuUsageM,
|
||||||
|
"mem_usage_mi": memUsageMi,
|
||||||
|
// Allocation
|
||||||
|
"pod_count": podCount,
|
||||||
|
"cpu_request_m": totalCPURequestM,
|
||||||
|
"mem_request_mi": totalMemRequestMi,
|
||||||
|
"cpu_alloc_pct": cpuAllocPct,
|
||||||
|
"mem_alloc_pct": memAllocPct,
|
||||||
|
"pod_alloc_pct": podAllocPct,
|
||||||
|
"cpu_capacity": capCPU,
|
||||||
|
"cpu_allocatable": allocCPU,
|
||||||
|
"mem_capacity_mb": capMem,
|
||||||
|
"mem_allocatable_mb": allocMem,
|
||||||
|
"pod_capacity": capPods,
|
||||||
|
"pod_allocatable": allocPods,
|
||||||
|
// Lists
|
||||||
|
"conditions": conditions,
|
||||||
|
"taints": taints,
|
||||||
|
"labels": labels,
|
||||||
|
"pods": podList,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "success", Data: detail})
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseCpuToMilli converts CPU resource string to millicores (e.g. "100m"→100, "2"→2000)
|
||||||
|
func parseCpuToMilli(val string) int {
|
||||||
|
if val == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(val, "m") {
|
||||||
|
var i int
|
||||||
|
fmt.Sscanf(strings.TrimSuffix(val, "m"), "%d", &i)
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
var i int
|
||||||
|
fmt.Sscanf(val, "%d", &i)
|
||||||
|
return i * 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseMemToMi converts memory resource string to Mi (e.g. "256Mi"→256, "1Gi"→1024, "512Ki"→0)
|
||||||
|
func parseMemToMi(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)
|
||||||
|
case "Ti":
|
||||||
|
return int64(num * 1024 * 1024)
|
||||||
|
default:
|
||||||
|
return int64(num / 1024 / 1024)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) GetCapacityAnalysis(c *gin.Context) {
|
||||||
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
|
||||||
|
ns := c.Query("namespace")
|
||||||
|
deps, err := client.ListDeployments()
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||||
|
|
||||||
|
type ContainerResource struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
CPUReq string `json:"cpu_request"`
|
||||||
|
CPULim string `json:"cpu_limit"`
|
||||||
|
MemReq string `json:"mem_request"`
|
||||||
|
MemLim string `json:"mem_limit"`
|
||||||
|
}
|
||||||
|
type CapacityRow struct {
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Replicas int `json:"replicas"`
|
||||||
|
Containers []ContainerResource `json:"containers"`
|
||||||
|
}
|
||||||
|
result := []CapacityRow{}
|
||||||
|
for _, d := range deps.Items {
|
||||||
|
if ns != "" && d.Metadata.Namespace != ns { continue }
|
||||||
|
containers := []ContainerResource{}
|
||||||
|
for _, c := range d.Spec.Template.Spec.Containers {
|
||||||
|
cpuReq := c.Resources.Requests["cpu"]
|
||||||
|
cpuLim := c.Resources.Limits["cpu"]
|
||||||
|
memReq := c.Resources.Requests["memory"]
|
||||||
|
memLim := c.Resources.Limits["memory"]
|
||||||
|
containers = append(containers, ContainerResource{c.Name, cpuReq, cpuLim, memReq, memLim})
|
||||||
|
}
|
||||||
|
result = append(result, CapacityRow{d.Metadata.Namespace, d.Metadata.Name, d.Spec.Replicas, containers})
|
||||||
|
}
|
||||||
|
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// YAML Apply (kubectl apply -f)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// ApplyYAML applies a YAML manifest to create or update a resource
|
||||||
@@ -0,0 +1,653 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"ezops/internal/models"
|
||||||
|
k8sclient "ezops/pkg/kubernetesclient"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// M2: inject pod metrics (best-effort) // Logan.Li
|
||||||
|
podMetrics := buildPodMetricsMap(client, namespace)
|
||||||
|
|
||||||
|
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"`
|
||||||
|
CPUUsageM string `json:"cpu_usage_m,omitempty"` // M2: e.g. "50m"
|
||||||
|
MemUsageMi string `json:"mem_usage_mi,omitempty"` // M2: e.g. "128Mi"
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
ps := 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,
|
||||||
|
}
|
||||||
|
if m, ok := podMetrics[p.Metadata.Namespace+"/"+p.Metadata.Name]; ok {
|
||||||
|
ps.CPUUsageM = m.CPU
|
||||||
|
ps.MemUsageMi = m.Mem
|
||||||
|
}
|
||||||
|
result = append(result, ps)
|
||||||
|
}
|
||||||
|
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) 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 WebSocket channel 协议
|
||||||
|
//
|
||||||
|
// 直接通过 K8s API 的 exec 端点(WebSocket channel.k8s.io v4 协议)建立
|
||||||
|
// 交互式终端,无需在容器中安装 kubectl。
|
||||||
|
//
|
||||||
|
// 数据流:
|
||||||
|
//
|
||||||
|
// Frontend WS (JSON) ←→ Backend ←→ K8s API WS (channel frames)
|
||||||
|
//
|
||||||
|
// K8s channel 协议: 每条消息 [1字节 channel ID] + [payload]
|
||||||
|
//
|
||||||
|
// 0 = stdin (client→server)
|
||||||
|
// 1 = stdout (server→client)
|
||||||
|
// 2 = stderr (server→client)
|
||||||
|
// 3 = error (server→client, JSON)
|
||||||
|
// 4 = resize (client→server, JSON {"Height","Width"})
|
||||||
|
func (h *KubernetesHandler) ExecTerminal(c *gin.Context) {
|
||||||
|
namespace := c.Param("namespace")
|
||||||
|
podName := c.Param("podName")
|
||||||
|
container := c.Query("container")
|
||||||
|
command := c.Query("command")
|
||||||
|
if command == "" {
|
||||||
|
command = "bash"
|
||||||
|
}
|
||||||
|
|
||||||
|
client, cluster, err := h.getK8sClientByCode(c)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build exec WebSocket URL — https→wss / http→ws
|
||||||
|
execURL := client.BaseURL
|
||||||
|
if strings.HasPrefix(execURL, "https://") {
|
||||||
|
execURL = "wss://" + strings.TrimPrefix(execURL, "https://")
|
||||||
|
} else {
|
||||||
|
execURL = "ws://" + strings.TrimPrefix(execURL, "http://")
|
||||||
|
}
|
||||||
|
params := url.Values{}
|
||||||
|
params.Set("command", command)
|
||||||
|
params.Set("stdin", "true")
|
||||||
|
params.Set("stdout", "true")
|
||||||
|
params.Set("stderr", "true")
|
||||||
|
params.Set("tty", "true")
|
||||||
|
if container != "" {
|
||||||
|
params.Set("container", container)
|
||||||
|
}
|
||||||
|
execURL += fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/exec?%s",
|
||||||
|
url.PathEscape(namespace), url.PathEscape(podName), params.Encode())
|
||||||
|
|
||||||
|
// Dial K8s API server via WebSocket with channel protocol subprotocol
|
||||||
|
k8sDialer := &websocket.Dialer{
|
||||||
|
TLSClientConfig: client.TLSConfig,
|
||||||
|
Subprotocols: []string{"v4.channel.k8s.io", "v3.channel.k8s.io", "v5.channel.k8s.io"},
|
||||||
|
HandshakeTimeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
k8sHeader := http.Header{}
|
||||||
|
if client.BearerToken != "" {
|
||||||
|
k8sHeader.Set("Authorization", "Bearer "+client.BearerToken)
|
||||||
|
}
|
||||||
|
k8sConn, _, err := k8sDialer.Dial(execURL, k8sHeader)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[K8sExec] dial K8s exec WS failed: %v", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "K8s exec 连接失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer k8sConn.Close()
|
||||||
|
|
||||||
|
// Upgrade frontend connection to WebSocket
|
||||||
|
frontendConn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer frontendConn.Close()
|
||||||
|
|
||||||
|
done := make(chan struct{}, 2)
|
||||||
|
|
||||||
|
// Frontend WS → K8s WS (stdin + resize)
|
||||||
|
go func() {
|
||||||
|
defer func() { done <- struct{}{} }()
|
||||||
|
for {
|
||||||
|
msgType, msg, err := frontendConn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = msgType
|
||||||
|
|
||||||
|
// Parse frontend JSON protocol: {status, data} or {status, cols, rows}
|
||||||
|
var wsMsg WSMessage
|
||||||
|
if err := json.Unmarshal(msg, &wsMsg); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if wsMsg.Status == 0 && wsMsg.Data != "" {
|
||||||
|
// stdin: prepend channel 0 byte
|
||||||
|
frame := make([]byte, 1+len(wsMsg.Data))
|
||||||
|
frame[0] = 0 // channel 0 = stdin
|
||||||
|
copy(frame[1:], wsMsg.Data)
|
||||||
|
if err := k8sConn.WriteMessage(websocket.BinaryMessage, frame); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if wsMsg.Status == 1 && wsMsg.Cols > 0 {
|
||||||
|
// resize: channel 4 + JSON
|
||||||
|
resizeJSON, _ := json.Marshal(struct {
|
||||||
|
Height uint16 `json:"Height"`
|
||||||
|
Width uint16 `json:"Width"`
|
||||||
|
}{Height: uint16(wsMsg.Rows), Width: uint16(wsMsg.Cols)})
|
||||||
|
frame := make([]byte, 1+len(resizeJSON))
|
||||||
|
frame[0] = 4 // channel 4 = resize
|
||||||
|
copy(frame[1:], resizeJSON)
|
||||||
|
if err := k8sConn.WriteMessage(websocket.BinaryMessage, frame); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// K8s WS → Frontend WS (stdout/stderr/error)
|
||||||
|
go func() {
|
||||||
|
defer func() { done <- struct{}{} }()
|
||||||
|
for {
|
||||||
|
msgType, msg, err := k8sConn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
// K8s connection closed — notify frontend
|
||||||
|
frontendConn.WriteJSON(WSMessage{Status: 2, Data: "\r\n\x1b[33m[K8s 连接已关闭]\x1b[0m\r\n"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = msgType
|
||||||
|
if len(msg) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
channel := msg[0]
|
||||||
|
payload := msg[1:]
|
||||||
|
|
||||||
|
switch channel {
|
||||||
|
case 1, 2: // stdout / stderr → forward to frontend as terminal output
|
||||||
|
frontendConn.WriteJSON(WSMessage{Status: 0, Data: string(payload)})
|
||||||
|
case 3: // error → forward as error message
|
||||||
|
errMsg := string(payload)
|
||||||
|
frontendConn.WriteJSON(WSMessage{Status: 2, Data: "\r\n\x1b[31m" + errMsg + "\x1b[0m\r\n"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-done
|
||||||
|
_ = cluster
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 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) ListPodsNS(c *gin.Context) {
|
||||||
|
h.listPodsWithNS(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) 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 }
|
||||||
|
// M2: inject pod metrics (best-effort) // Logan.Li
|
||||||
|
podMetrics := buildPodMetricsMap(client, ns)
|
||||||
|
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"`
|
||||||
|
CPUUsageM string `json:"cpu_usage_m,omitempty"`
|
||||||
|
MemUsageMi string `json:"mem_usage_mi,omitempty"`
|
||||||
|
}
|
||||||
|
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 }
|
||||||
|
ps := 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,
|
||||||
|
}
|
||||||
|
if m, ok := podMetrics[p.Metadata.Namespace+"/"+p.Metadata.Name]; ok {
|
||||||
|
ps.CPUUsageM = m.CPU
|
||||||
|
ps.MemUsageMi = m.Mem
|
||||||
|
}
|
||||||
|
result = append(result, ps)
|
||||||
|
}
|
||||||
|
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||||
|
}
|
||||||
|
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")
|
||||||
|
stream := c.Query("stream") == "true"
|
||||||
|
// 日志增强参数:container / sinceTime / previous / tailLines(兼容 tail) // Logan.Li
|
||||||
|
tailLines := 0
|
||||||
|
if v := c.Query("tailLines"); v != "" {
|
||||||
|
tailLines, _ = strconv.Atoi(v)
|
||||||
|
}
|
||||||
|
if tailLines == 0 {
|
||||||
|
tailLines, _ = strconv.Atoi(c.DefaultQuery("tail", "200"))
|
||||||
|
}
|
||||||
|
opts := k8sclient.LogOptions{
|
||||||
|
Container: c.Query("container"),
|
||||||
|
TailLines: tailLines,
|
||||||
|
SinceTime: c.Query("sinceTime"),
|
||||||
|
Previous: c.Query("previous") == "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.GetPodLogsStreamWithOptions(ns, podName, opts)
|
||||||
|
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.GetPodLogsWithOptions(ns, podName, opts)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "获取日志失败: " + err.Error()}); return }
|
||||||
|
c.JSON(200, models.Response{Code: 200, Message: "success", 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})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// All-namespace handlers (clusterCode-based, no namespace filter)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// ListDeploymentsAllNS lists all deployments across all namespaces using clusterCode.
|
||||||
|
func (h *KubernetesHandler) ListPodsAllNS(c *gin.Context) {
|
||||||
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
pods, err := client.ListPods()
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||||
|
// M2: inject pod metrics (best-effort) // Logan.Li
|
||||||
|
podMetrics := buildPodMetricsMap(client, "")
|
||||||
|
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"`
|
||||||
|
CPUUsageM string `json:"cpu_usage_m,omitempty"`
|
||||||
|
MemUsageMi string `json:"mem_usage_mi,omitempty"`
|
||||||
|
}
|
||||||
|
result := []PodSummary{}
|
||||||
|
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 }
|
||||||
|
ps := 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,
|
||||||
|
}
|
||||||
|
if m, ok := podMetrics[p.Metadata.Namespace+"/"+p.Metadata.Name]; ok {
|
||||||
|
ps.CPUUsageM = m.CPU
|
||||||
|
ps.MemUsageMi = m.Mem
|
||||||
|
}
|
||||||
|
result = append(result, ps)
|
||||||
|
}
|
||||||
|
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListHPAsAllNS lists all HPAs across all namespaces using clusterCode.
|
||||||
|
func buildPodMetricsMap(client *k8sclient.Client, namespace string) map[string]struct{ CPU, Mem string } {
|
||||||
|
m := map[string]struct{ CPU, Mem string }{}
|
||||||
|
if pm, err := client.ListPodMetrics(namespace); err == nil {
|
||||||
|
for _, item := range pm.Items {
|
||||||
|
totalCPU := 0
|
||||||
|
totalMemMi := 0
|
||||||
|
for _, c := range item.Containers {
|
||||||
|
totalCPU += parseCpuNanoToMilli(c.Usage["cpu"])
|
||||||
|
totalMemMi += parseMemKiToMi(c.Usage["memory"])
|
||||||
|
}
|
||||||
|
key := item.Metadata.Namespace + "/" + item.Metadata.Name
|
||||||
|
m[key] = struct{ CPU, Mem string }{
|
||||||
|
CPU: fmt.Sprintf("%dm", totalCPU),
|
||||||
|
Mem: fmt.Sprintf("%dMi", totalMemMi),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("[K8s] ListPodMetrics failed (metrics-server not installed?): %v", err)
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatCpuFromNano converts K8s metrics CPU (nanocores "80410173n") to millicores "80m"
|
||||||
|
func formatCpuFromNano(raw string) string {
|
||||||
|
if raw == "" { return "" }
|
||||||
|
val := raw
|
||||||
|
val = strings.TrimSuffix(val, "n")
|
||||||
|
var nano int64
|
||||||
|
fmt.Sscanf(val, "%d", &nano)
|
||||||
|
milli := nano / 1000000
|
||||||
|
if milli < 1 { milli = 1 }
|
||||||
|
return fmt.Sprintf("%dm", milli)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseCpuNanoToMilli converts K8s metrics CPU (nanocores "100354992n") to millicores (int) // Logan.Li
|
||||||
|
func parseCpuNanoToMilli(raw string) int {
|
||||||
|
if raw == "" { return 0 }
|
||||||
|
val := strings.TrimSuffix(raw, "n")
|
||||||
|
var nano int64
|
||||||
|
fmt.Sscanf(val, "%d", &nano)
|
||||||
|
milli := nano / 1000000
|
||||||
|
if milli < 1 { milli = 1 }
|
||||||
|
return int(milli)
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatMemFromKi converts K8s metrics memory (Ki "2154292Ki") to Mi "2105Mi"
|
||||||
|
func formatMemFromKi(raw string) string {
|
||||||
|
if raw == "" { return "" }
|
||||||
|
val := strings.TrimSuffix(raw, "Ki")
|
||||||
|
var ki int64
|
||||||
|
fmt.Sscanf(val, "%d", &ki)
|
||||||
|
mi := ki / 1024
|
||||||
|
if mi < 1 { mi = 1 }
|
||||||
|
return fmt.Sprintf("%dMi", mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseMemKiToMi converts K8s metrics memory (Ki "2154292Ki") to Mi (int) // Logan.Li
|
||||||
|
func parseMemKiToMi(raw string) int {
|
||||||
|
if raw == "" { return 0 }
|
||||||
|
val := strings.TrimSuffix(raw, "Ki")
|
||||||
|
var ki int64
|
||||||
|
fmt.Sscanf(val, "%d", &ki)
|
||||||
|
return int(ki / 1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// GetResourceYAMLNS returns the YAML of a K8s resource
|
||||||
|
func (h *KubernetesHandler) GetPodMetrics(c *gin.Context) {
|
||||||
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
|
||||||
|
ns := c.Query("namespace")
|
||||||
|
metrics, err := client.ListPodMetrics(ns)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "metrics API error: " + err.Error()}); return }
|
||||||
|
|
||||||
|
type ContainerMetric struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
CPU string `json:"cpu"`
|
||||||
|
Memory string `json:"memory"`
|
||||||
|
}
|
||||||
|
type PodMetric struct {
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Containers []ContainerMetric `json:"containers"`
|
||||||
|
}
|
||||||
|
result := []PodMetric{}
|
||||||
|
for _, pm := range metrics.Items {
|
||||||
|
containers := []ContainerMetric{}
|
||||||
|
for _, c := range pm.Containers {
|
||||||
|
containers = append(containers, ContainerMetric{c.Name, c.Usage["cpu"], c.Usage["memory"]})
|
||||||
|
}
|
||||||
|
result = append(result, PodMetric{pm.Metadata.Namespace, pm.Metadata.Name, containers})
|
||||||
|
}
|
||||||
|
c.JSON(200, models.Response{Code: 200, Data: result})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Events (kubectl get events)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// ListEvents returns cluster events (optionally filtered by namespace and type)
|
||||||
|
func (h *KubernetesHandler) DeletePodNS(c *gin.Context) {
|
||||||
|
h.deletePodWithNS(c, c.Param("namespace"))
|
||||||
|
}
|
||||||
|
func (h *KubernetesHandler) deletePodWithNS(c *gin.Context, ns string) {
|
||||||
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
|
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||||
|
podName := c.Param("podName")
|
||||||
|
if podName == "" { c.JSON(400, models.Response{Code: 400, Message: "缺少 podName"}); return }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
if err := client.DeletePod(ns, podName); err != nil {
|
||||||
|
c.JSON(500, models.Response{Code: 500, Message: "删除 Pod 失败: " + err.Error()}); return
|
||||||
|
}
|
||||||
|
c.JSON(200, models.Response{Code: 200, Message: fmt.Sprintf("Pod %s 已删除", podName)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Pod 文件列表 / 下载(复用 exec 能力) // Logan.Li
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// cleanExecOutput 清理 exec 输出中残留的 SPDY 帧头等非打印字节(best-effort)
|
||||||
|
func cleanExecOutput(raw string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
for _, r := range raw {
|
||||||
|
if r == '\n' || r == '\r' || r == ' ' || (r >= 0x20 && r < 0x7F) {
|
||||||
|
b.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListPodFilesNS 列出 Pod 内指定路径的文件
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"ezops/internal/models"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *KubernetesHandler) ListPodFilesNS(c *gin.Context) {
|
||||||
|
h.listPodFilesWithNS(c, c.Param("namespace"))
|
||||||
|
}
|
||||||
|
func (h *KubernetesHandler) listPodFilesWithNS(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 {
|
||||||
|
Path string `json:"path"`
|
||||||
|
Container string `json:"container"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, models.Response{Code: 400, Message: "参数错误: " + err.Error()}); return }
|
||||||
|
if req.Path == "" { req.Path = "/" }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
// ls -la — BusyBox ignores --time-style, use default "Mon DD HH:MM" format
|
||||||
|
cmd := []string{"ls", "-la", req.Path}
|
||||||
|
result, err := client.ExecInPod(ns, podName, req.Container, cmd)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "执行 ls 失败: " + err.Error()}); return }
|
||||||
|
|
||||||
|
type FileInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Size string `json:"size"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
ModTime string `json:"mod_time"`
|
||||||
|
IsDir bool `json:"is_dir"`
|
||||||
|
}
|
||||||
|
files := []FileInfo{}
|
||||||
|
output := result.Stdout // WebSocket output is already clean text
|
||||||
|
for _, line := range strings.Split(output, "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" || strings.HasPrefix(line, "total ") { continue }
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) < 7 { continue }
|
||||||
|
mode := fields[0]
|
||||||
|
if len(mode) == 0 || (mode[0] != 'd' && mode[0] != '-' && mode[0] != 'l') { continue }
|
||||||
|
size := fields[4]
|
||||||
|
// BusyBox ls date format: "Jun 23 02:17" (3 fields: month day time)
|
||||||
|
// GNU ls --time-style: "2026-06-23T02:17:00" (1 field)
|
||||||
|
// Detect format: if fields[5] contains ":" it's time-only (BusyBox style)
|
||||||
|
var name, modTime string
|
||||||
|
if strings.Contains(fields[5], ":") || strings.Contains(fields[5], "T") {
|
||||||
|
// GNU style: single timestamp field
|
||||||
|
modTime = fields[5]
|
||||||
|
name = strings.Join(fields[6:], " ")
|
||||||
|
} else {
|
||||||
|
// BusyBox style: "Mon DD HH:MM" = 3 fields starting at index 5
|
||||||
|
modTime = strings.Join(fields[5:8], " ")
|
||||||
|
name = strings.Join(fields[8:], " ")
|
||||||
|
}
|
||||||
|
if name == "" || name == "." || name == ".." { continue }
|
||||||
|
files = append(files, FileInfo{
|
||||||
|
Name: name,
|
||||||
|
Size: size,
|
||||||
|
Mode: mode,
|
||||||
|
ModTime: modTime,
|
||||||
|
IsDir: mode[0] == 'd',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
c.JSON(200, models.Response{Code: 200, Message: "success", Data: gin.H{"path": req.Path, "files": files}})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadPodFileNS 下载 Pod 内指定路径的文件(通过 exec cat)
|
||||||
|
func (h *KubernetesHandler) DownloadPodFileNS(c *gin.Context) {
|
||||||
|
h.downloadPodFileWithNS(c, c.Param("namespace"))
|
||||||
|
}
|
||||||
|
func (h *KubernetesHandler) downloadPodFileWithNS(c *gin.Context, ns string) {
|
||||||
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
|
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||||
|
podName := c.Param("podName")
|
||||||
|
filePath := c.Query("path")
|
||||||
|
container := c.Query("container")
|
||||||
|
if filePath == "" { c.JSON(400, models.Response{Code: 400, Message: "path 参数不能为空"}); return }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
cmd := []string{"cat", filePath}
|
||||||
|
result, err := client.ExecInPod(ns, podName, container, cmd)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "读取文件失败: " + err.Error()}); return }
|
||||||
|
content := result.Stdout
|
||||||
|
fileName := path.Base(filePath)
|
||||||
|
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileName))
|
||||||
|
c.Header("Content-Type", "application/octet-stream")
|
||||||
|
c.String(200, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadPodFileNS 上传文件到 Pod 容器内指定目录(通过 exec base64 分块写入)
|
||||||
|
func (h *KubernetesHandler) UploadPodFileNS(c *gin.Context) {
|
||||||
|
h.uploadPodFileWithNS(c, c.Param("namespace"))
|
||||||
|
}
|
||||||
|
func (h *KubernetesHandler) uploadPodFileWithNS(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.PostForm("container")
|
||||||
|
targetDir := c.PostForm("path")
|
||||||
|
if targetDir == "" {
|
||||||
|
targetDir = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取上传的文件
|
||||||
|
file, header, err := c.Request.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, models.Response{Code: 400, Message: "文件上传失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// 限制文件大小 10MB
|
||||||
|
const maxUploadSize = 10 << 20 // 10MB
|
||||||
|
if header.Size > maxUploadSize {
|
||||||
|
c.JSON(400, models.Response{Code: 400, Message: fmt.Sprintf("文件过大: %d bytes, 上限 %d bytes", header.Size, maxUploadSize)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, models.Response{Code: 500, Message: "读取文件失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, models.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 目标完整路径
|
||||||
|
targetFile := strings.TrimRight(targetDir, "/") + "/" + header.Filename
|
||||||
|
|
||||||
|
// 确保目标目录存在
|
||||||
|
_, _ = client.ExecInPod(ns, podName, container, []string{"mkdir", "-p", targetDir})
|
||||||
|
|
||||||
|
// base64 分块写入:每块 3000 字节 base64 (~2250 bytes 二进制),保证 URL 长度安全
|
||||||
|
encoded := base64.StdEncoding.EncodeToString(content)
|
||||||
|
const chunkSize = 3000
|
||||||
|
first := true
|
||||||
|
for i := 0; i < len(encoded); i += chunkSize {
|
||||||
|
end := i + chunkSize
|
||||||
|
if end > len(encoded) {
|
||||||
|
end = len(encoded)
|
||||||
|
}
|
||||||
|
chunk := encoded[i:end]
|
||||||
|
var cmd []string
|
||||||
|
if first {
|
||||||
|
// 首块:创建/覆盖文件
|
||||||
|
cmd = []string{"sh", "-c", fmt.Sprintf("echo -n '%s' | base64 -d > '%s'", chunk, targetFile)}
|
||||||
|
first = false
|
||||||
|
} else {
|
||||||
|
// 后续块:追加
|
||||||
|
cmd = []string{"sh", "-c", fmt.Sprintf("echo -n '%s' | base64 -d >> '%s'", chunk, targetFile)}
|
||||||
|
}
|
||||||
|
result, err := client.ExecInPod(ns, podName, container, cmd)
|
||||||
|
if err != nil {
|
||||||
|
stderr := ""
|
||||||
|
if result != nil {
|
||||||
|
stderr = result.Stderr
|
||||||
|
}
|
||||||
|
c.JSON(500, models.Response{Code: 500, Message: fmt.Sprintf("写入文件失败 (chunk %d): %s\n%s", i/chunkSize, err.Error(), stderr)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证文件大小
|
||||||
|
verifyResult, verifyErr := client.ExecInPod(ns, podName, container, []string{"stat", "-c", "%s", targetFile})
|
||||||
|
writtenSize := ""
|
||||||
|
if verifyErr == nil && verifyResult != nil {
|
||||||
|
writtenSize = strings.TrimSpace(verifyResult.Stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[UploadPodFile] %s/%s → %s (%d bytes, %d chunks)", ns, podName, targetFile, len(content), (len(encoded)+chunkSize-1)/chunkSize)
|
||||||
|
|
||||||
|
c.JSON(200, models.Response{
|
||||||
|
Code: 200,
|
||||||
|
Message: "上传成功",
|
||||||
|
Data: gin.H{
|
||||||
|
"path": targetFile,
|
||||||
|
"name": header.Filename,
|
||||||
|
"size": len(content),
|
||||||
|
"verify": writtenSize,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// M13: 多集群资源对比看板 (Multi-Cluster Comparison Dashboard) // Logan.Li
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// GetMultiClusterComparison 聚合所有集群的资源数据进行对比
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"ezops/internal/models"
|
||||||
|
|
||||||
|
"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, 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) 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) 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, 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 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, 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 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, models.Response{Code: 200, Data: result})
|
||||||
|
}
|
||||||
|
func (h *KubernetesHandler) ListPVCsNS(c *gin.Context) {
|
||||||
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
ns := c.Param("namespace")
|
||||||
|
pvcs, err := client.ListPVCs(ns)
|
||||||
|
if err != nil { c.JSON(500, models.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, models.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, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
if err := client.DeletePVC(c.Param("namespace"), c.Param("name")); err != nil {
|
||||||
|
c.JSON(500, models.Response{Code: 500, Message: "删除失败: " + err.Error()}); return
|
||||||
|
}
|
||||||
|
c.JSON(200, models.Response{Code: 200, Message: "已删除"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// StatefulSet / DaemonSet handlers (code-based)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// ListStatefulSetsNS lists StatefulSets in a namespace
|
||||||
@@ -0,0 +1,696 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"ezops/internal/models"
|
||||||
|
|
||||||
|
"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, 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) 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: "重启指令已下发"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PauseDeployment 暂停 Deployment 滚动更新(spec.paused=true)
|
||||||
|
func (h *KubernetesHandler) PauseDeployment(c *gin.Context) {
|
||||||
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
|
namespace := c.Param("namespace")
|
||||||
|
name := c.Param("name")
|
||||||
|
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.PauseDeployment(namespace, name); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "暂停更新失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已暂停滚动更新"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResumeDeployment 恢复 Deployment 滚动更新(spec.paused=false)
|
||||||
|
func (h *KubernetesHandler) ResumeDeployment(c *gin.Context) {
|
||||||
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
|
namespace := c.Param("namespace")
|
||||||
|
name := c.Param("name")
|
||||||
|
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.ResumeDeployment(namespace, name); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "恢复更新失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已恢复滚动更新"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// HPA 管理
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// 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, 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) ListDeploymentsNS(c *gin.Context) {
|
||||||
|
h.listDeploymentsWithNS(c, c.Param("namespace"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDeploymentNS 获取单个 Deployment 详情(避免前端 404)// Logan.Li
|
||||||
|
func (h *KubernetesHandler) GetDeploymentNS(c *gin.Context) {
|
||||||
|
ns := c.Param("namespace")
|
||||||
|
name := c.Param("name")
|
||||||
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
|
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
deps, err := client.ListDeployments()
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||||
|
for _, d := range deps.Items {
|
||||||
|
if d.Metadata.Namespace == ns && d.Metadata.Name == name {
|
||||||
|
image := ""
|
||||||
|
if len(d.Spec.Template.Spec.Containers) > 0 { image = d.Spec.Template.Spec.Containers[0].Image }
|
||||||
|
c.JSON(200, models.Response{Code: 200, Data: map[string]interface{}{
|
||||||
|
"namespace": d.Metadata.Namespace,
|
||||||
|
"name": d.Metadata.Name,
|
||||||
|
"replicas": d.Spec.Replicas,
|
||||||
|
"available_replicas": d.Status.AvailableReplicas,
|
||||||
|
"ready_replicas": d.Status.ReadyReplicas,
|
||||||
|
"image": image,
|
||||||
|
"paused": d.Spec.Paused,
|
||||||
|
}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.JSON(404, models.Response{Code: 404, Message: "Deployment not found"})
|
||||||
|
}
|
||||||
|
func (h *KubernetesHandler) 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, 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 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, models.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, 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) pauseDeploymentWithNS(c *gin.Context, ns string) {
|
||||||
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
|
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
if err := client.PauseDeployment(ns, c.Param("name")); err != nil { c.JSON(500, models.Response{Code: 500, Message: "暂停更新失败: " + err.Error()}); return }
|
||||||
|
c.JSON(200, models.Response{Code: 200, Message: "已暂停滚动更新"})
|
||||||
|
}
|
||||||
|
func (h *KubernetesHandler) resumeDeploymentWithNS(c *gin.Context, ns string) {
|
||||||
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
|
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
if err := client.ResumeDeployment(ns, c.Param("name")); err != nil { c.JSON(500, models.Response{Code: 500, Message: "恢复更新失败: " + err.Error()}); return }
|
||||||
|
c.JSON(200, models.Response{Code: 200, Message: "已恢复滚动更新"})
|
||||||
|
}
|
||||||
|
func (h *KubernetesHandler) ListDeploymentsAllNS(c *gin.Context) {
|
||||||
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
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 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, models.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, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
sts, err := client.ListStatefulSets()
|
||||||
|
if err != nil { c.JSON(500, models.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, models.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, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
ds, err := client.ListDaemonSets()
|
||||||
|
if err != nil { c.JSON(500, models.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, models.Response{Code: 200, Data: result})
|
||||||
|
}
|
||||||
|
// ScaleStatefulSetNS scales a StatefulSet (Kuboard code route)
|
||||||
|
func (h *KubernetesHandler) ScaleStatefulSetNS(c *gin.Context) {
|
||||||
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
ns := c.Param("namespace")
|
||||||
|
name := c.Param("name")
|
||||||
|
var req struct{ Replicas int `json:"replicas"` }
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, models.Response{Code: 400, Message: "参数错误"}); return }
|
||||||
|
if err := client.ScaleStatefulSet(ns, name, req.Replicas); err != nil {
|
||||||
|
c.JSON(500, models.Response{Code: 500, Message: "扩缩容失败: " + err.Error()}); return
|
||||||
|
}
|
||||||
|
c.JSON(200, models.Response{Code: 200, Message: "StatefulSet 副本数已更新为 " + strconv.Itoa(req.Replicas)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScaleStatefulSet scales a StatefulSet (ID route)
|
||||||
|
func (h *KubernetesHandler) ScaleStatefulSet(c *gin.Context) {
|
||||||
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
|
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||||
|
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
ns := c.Param("namespace")
|
||||||
|
name := c.Param("name")
|
||||||
|
var req struct{ Replicas int `json:"replicas"` }
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, models.Response{Code: 400, Message: "参数错误"}); return }
|
||||||
|
if err := client.ScaleStatefulSet(ns, name, req.Replicas); err != nil {
|
||||||
|
c.JSON(500, models.Response{Code: 500, Message: "扩缩容失败: " + err.Error()}); return
|
||||||
|
}
|
||||||
|
c.JSON(200, models.Response{Code: 200, Message: "StatefulSet 副本数已更新为 " + strconv.Itoa(req.Replicas)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestartStatefulSetNS restarts a StatefulSet (Kuboard code route)
|
||||||
|
func (h *KubernetesHandler) RestartStatefulSetNS(c *gin.Context) {
|
||||||
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
ns := c.Param("namespace")
|
||||||
|
name := c.Param("name")
|
||||||
|
if err := client.RestartStatefulSet(ns, name); err != nil {
|
||||||
|
c.JSON(500, models.Response{Code: 500, Message: "重启失败: " + err.Error()}); return
|
||||||
|
}
|
||||||
|
c.JSON(200, models.Response{Code: 200, Message: "StatefulSet 滚动重启已触发"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestartStatefulSet restarts a StatefulSet (ID route)
|
||||||
|
func (h *KubernetesHandler) RestartStatefulSet(c *gin.Context) {
|
||||||
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
|
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||||
|
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
ns := c.Param("namespace")
|
||||||
|
name := c.Param("name")
|
||||||
|
if err := client.RestartStatefulSet(ns, name); err != nil {
|
||||||
|
c.JSON(500, models.Response{Code: 500, Message: "重启失败: " + err.Error()}); return
|
||||||
|
}
|
||||||
|
c.JSON(200, models.Response{Code: 200, Message: "StatefulSet 滚动重启已触发"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestartDaemonSetNS restarts a DaemonSet (Kuboard code route)
|
||||||
|
func (h *KubernetesHandler) RestartDaemonSetNS(c *gin.Context) {
|
||||||
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
ns := c.Param("namespace")
|
||||||
|
name := c.Param("name")
|
||||||
|
if err := client.RestartDaemonSet(ns, name); err != nil {
|
||||||
|
c.JSON(500, models.Response{Code: 500, Message: "重启失败: " + err.Error()}); return
|
||||||
|
}
|
||||||
|
c.JSON(200, models.Response{Code: 200, Message: "DaemonSet 滚动重启已触发"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestartDaemonSet restarts a DaemonSet (ID route)
|
||||||
|
func (h *KubernetesHandler) RestartDaemonSet(c *gin.Context) {
|
||||||
|
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||||
|
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||||
|
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
ns := c.Param("namespace")
|
||||||
|
name := c.Param("name")
|
||||||
|
if err := client.RestartDaemonSet(ns, name); err != nil {
|
||||||
|
c.JSON(500, models.Response{Code: 500, Message: "重启失败: " + err.Error()}); return
|
||||||
|
}
|
||||||
|
c.JSON(200, models.Response{Code: 200, Message: "DaemonSet 滚动重启已触发"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatefulSetDetailNS returns a single StatefulSet detail (Kuboard code route)
|
||||||
|
func (h *KubernetesHandler) GetStatefulSetDetailNS(c *gin.Context) {
|
||||||
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
ns := c.Param("namespace")
|
||||||
|
name := c.Param("name")
|
||||||
|
sts, err := client.GetStatefulSet(ns, name)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||||
|
c.JSON(200, models.Response{Code: 200, Data: sts})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDaemonSetDetailNS returns a single DaemonSet detail (Kuboard code route)
|
||||||
|
func (h *KubernetesHandler) GetDaemonSetDetailNS(c *gin.Context) {
|
||||||
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
ns := c.Param("namespace")
|
||||||
|
name := c.Param("name")
|
||||||
|
ds, err := client.GetDaemonSet(ns, name)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "K8s API error: " + err.Error()}); return }
|
||||||
|
c.JSON(200, models.Response{Code: 200, Data: ds})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Pod Metrics (kubectl top pods) — per-container CPU/Memory usage via metrics-server
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// 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, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
items, err := client.ListReplicaSetsRaw(ns)
|
||||||
|
if err != nil { c.JSON(500, models.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, models.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, models.Response{Code: 400, Message: "缺少 deployment name"}); return }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
items, err := client.ListReplicaSetsRaw(ns)
|
||||||
|
if err != nil { c.JSON(500, models.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, models.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, models.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, models.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, models.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, models.Response{Code: 404, Message: fmt.Sprintf("revision %d 的 ReplicaSet 未找到", req.Revision)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if targetRS == "" { c.JSON(400, models.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, models.Response{Code: 500, Message: "获取 ReplicaSet 失败: " + err.Error()}); return }
|
||||||
|
specMap, ok := rsJSON["spec"].(map[string]interface{})
|
||||||
|
if !ok { c.JSON(500, models.Response{Code: 500, Message: "ReplicaSet 缺少 spec"}); return }
|
||||||
|
template, ok := specMap["template"].(map[string]interface{})
|
||||||
|
if !ok { c.JSON(500, models.Response{Code: 500, Message: "ReplicaSet 缺少 template"}); return }
|
||||||
|
|
||||||
|
if err := client.PatchDeploymentSpecTemplate(ns, deploymentName, template); err != nil {
|
||||||
|
c.JSON(500, models.Response{Code: 500, Message: "回滚失败: " + err.Error()}); return
|
||||||
|
}
|
||||||
|
c.JSON(200, models.Response{
|
||||||
|
Code: 200,
|
||||||
|
Message: fmt.Sprintf("Deployment %s 已回滚到 %s", deploymentName, targetRS),
|
||||||
|
Data: gin.H{"replicaSet": targetRS, "revision": req.Revision},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Pod 删除 // Logan.Li
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// DeletePodNS 删除指定 Pod
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"ezops/internal/models"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *KubernetesHandler) GetResourceYAMLNS(c *gin.Context) {
|
||||||
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
ns := c.Param("namespace")
|
||||||
|
resource := c.Param("resource")
|
||||||
|
// Fallback: specific routes (statefulsets/:name/yaml, daemonsets/:name/yaml) have no :resource param
|
||||||
|
if resource == "" {
|
||||||
|
pathParts := strings.Split(c.Request.URL.Path, "/")
|
||||||
|
for _, p := range pathParts {
|
||||||
|
if p == "statefulsets" || p == "daemonsets" || p == "deployments" || p == "services" || p == "configmaps" || p == "secrets" {
|
||||||
|
resource = p
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
name := c.Param("name")
|
||||||
|
// Build API path based on resource type
|
||||||
|
var apiPath string
|
||||||
|
switch resource {
|
||||||
|
case "deployments", "deployment":
|
||||||
|
apiPath = fmt.Sprintf("/apis/apps/v1/namespaces/%s/deployments/%s", ns, name)
|
||||||
|
case "statefulsets", "statefulset":
|
||||||
|
apiPath = fmt.Sprintf("/apis/apps/v1/namespaces/%s/statefulsets/%s", ns, name)
|
||||||
|
case "daemonsets", "daemonset":
|
||||||
|
apiPath = fmt.Sprintf("/apis/apps/v1/namespaces/%s/daemonsets/%s", ns, name)
|
||||||
|
case "replicasets", "replicaset":
|
||||||
|
apiPath = fmt.Sprintf("/apis/apps/v1/namespaces/%s/replicasets/%s", ns, name)
|
||||||
|
case "pods", "pod":
|
||||||
|
apiPath = fmt.Sprintf("/api/v1/namespaces/%s/pods/%s", ns, name)
|
||||||
|
case "services", "service":
|
||||||
|
apiPath = fmt.Sprintf("/api/v1/namespaces/%s/services/%s", ns, name)
|
||||||
|
case "ingresses", "ingress":
|
||||||
|
apiPath = fmt.Sprintf("/apis/networking.k8s.io/v1/namespaces/%s/ingresses/%s", ns, name)
|
||||||
|
case "configmaps", "configmap":
|
||||||
|
apiPath = fmt.Sprintf("/api/v1/namespaces/%s/configmaps/%s", ns, name)
|
||||||
|
case "secrets", "secret":
|
||||||
|
apiPath = fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s", ns, name)
|
||||||
|
case "pvcs", "persistentvolumeclaim":
|
||||||
|
apiPath = fmt.Sprintf("/api/v1/namespaces/%s/persistentvolumeclaims/%s", ns, name)
|
||||||
|
case "hpas", "hpa", "horizontalpodautoscaler":
|
||||||
|
apiPath = fmt.Sprintf("/apis/autoscaling/v2/namespaces/%s/horizontalpodautoscalers/%s", ns, name)
|
||||||
|
case "cronjobs", "cronjob":
|
||||||
|
apiPath = fmt.Sprintf("/apis/batch/v1/namespaces/%s/cronjobs/%s", ns, name)
|
||||||
|
case "serviceaccounts", "serviceaccount":
|
||||||
|
apiPath = fmt.Sprintf("/api/v1/namespaces/%s/serviceaccounts/%s", ns, name)
|
||||||
|
case "roles", "role":
|
||||||
|
apiPath = fmt.Sprintf("/apis/rbac.authorization.k8s.io/v1/namespaces/%s/roles/%s", ns, name)
|
||||||
|
case "rolebindings", "rolebinding":
|
||||||
|
apiPath = fmt.Sprintf("/apis/rbac.authorization.k8s.io/v1/namespaces/%s/rolebindings/%s", ns, name)
|
||||||
|
default:
|
||||||
|
c.JSON(400, models.Response{Code: 400, Message: "unsupported resource type: " + resource}); return
|
||||||
|
}
|
||||||
|
yamlBytes, err := client.GetResourceYAML(apiPath)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "获取YAML失败: " + err.Error()}); return }
|
||||||
|
c.Header("Content-Type", "application/yaml")
|
||||||
|
c.String(200, string(yamlBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateResourceYAMLNS applies YAML to update a K8s resource
|
||||||
|
func (h *KubernetesHandler) UpdateResourceYAMLNS(c *gin.Context) {
|
||||||
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
ns := c.Param("namespace")
|
||||||
|
resource := c.Param("resource")
|
||||||
|
// Fallback: specific routes (statefulsets/:name/yaml, daemonsets/:name/yaml) have no :resource param
|
||||||
|
if resource == "" {
|
||||||
|
pathParts := strings.Split(c.Request.URL.Path, "/")
|
||||||
|
for _, p := range pathParts {
|
||||||
|
if p == "statefulsets" || p == "daemonsets" || p == "deployments" || p == "services" || p == "configmaps" || p == "secrets" {
|
||||||
|
resource = p
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
name := c.Param("name")
|
||||||
|
yamlData, err := io.ReadAll(c.Request.Body)
|
||||||
|
if err != nil { c.JSON(400, models.Response{Code: 400, Message: "读取body失败"}); return }
|
||||||
|
var apiPath string
|
||||||
|
switch resource {
|
||||||
|
case "deployments", "deployment":
|
||||||
|
apiPath = fmt.Sprintf("/apis/apps/v1/namespaces/%s/deployments/%s", ns, name)
|
||||||
|
case "statefulsets", "statefulset":
|
||||||
|
apiPath = fmt.Sprintf("/apis/apps/v1/namespaces/%s/statefulsets/%s", ns, name)
|
||||||
|
case "daemonsets", "daemonset":
|
||||||
|
apiPath = fmt.Sprintf("/apis/apps/v1/namespaces/%s/daemonsets/%s", ns, name)
|
||||||
|
case "replicasets", "replicaset":
|
||||||
|
apiPath = fmt.Sprintf("/apis/apps/v1/namespaces/%s/replicasets/%s", ns, name)
|
||||||
|
case "pods", "pod":
|
||||||
|
apiPath = fmt.Sprintf("/api/v1/namespaces/%s/pods/%s", ns, name)
|
||||||
|
case "services", "service":
|
||||||
|
apiPath = fmt.Sprintf("/api/v1/namespaces/%s/services/%s", ns, name)
|
||||||
|
case "ingresses", "ingress":
|
||||||
|
apiPath = fmt.Sprintf("/apis/networking.k8s.io/v1/namespaces/%s/ingresses/%s", ns, name)
|
||||||
|
case "configmaps", "configmap":
|
||||||
|
apiPath = fmt.Sprintf("/api/v1/namespaces/%s/configmaps/%s", ns, name)
|
||||||
|
case "secrets", "secret":
|
||||||
|
apiPath = fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s", ns, name)
|
||||||
|
case "pvcs", "persistentvolumeclaim":
|
||||||
|
apiPath = fmt.Sprintf("/api/v1/namespaces/%s/persistentvolumeclaims/%s", ns, name)
|
||||||
|
case "hpas", "hpa", "horizontalpodautoscaler":
|
||||||
|
apiPath = fmt.Sprintf("/apis/autoscaling/v2/namespaces/%s/horizontalpodautoscalers/%s", ns, name)
|
||||||
|
case "cronjobs", "cronjob":
|
||||||
|
apiPath = fmt.Sprintf("/apis/batch/v1/namespaces/%s/cronjobs/%s", ns, name)
|
||||||
|
case "serviceaccounts", "serviceaccount":
|
||||||
|
apiPath = fmt.Sprintf("/api/v1/namespaces/%s/serviceaccounts/%s", ns, name)
|
||||||
|
case "roles", "role":
|
||||||
|
apiPath = fmt.Sprintf("/apis/rbac.authorization.k8s.io/v1/namespaces/%s/roles/%s", ns, name)
|
||||||
|
case "rolebindings", "rolebinding":
|
||||||
|
apiPath = fmt.Sprintf("/apis/rbac.authorization.k8s.io/v1/namespaces/%s/rolebindings/%s", ns, name)
|
||||||
|
default:
|
||||||
|
c.JSON(400, models.Response{Code: 400, Message: "unsupported resource type: " + resource}); return
|
||||||
|
}
|
||||||
|
if err := client.UpdateResourceYAML(apiPath, yamlData); err != nil {
|
||||||
|
c.JSON(500, models.Response{Code: 500, Message: "更新失败: " + err.Error()}); return
|
||||||
|
}
|
||||||
|
c.JSON(200, models.Response{Code: 200, Message: "YAML已应用"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// resourceAPIPath builds the K8s API path for common resource types (shared by Get/Update/Delete)
|
||||||
|
func resourceAPIPath(ns, resource, name string) (string, error) {
|
||||||
|
switch resource {
|
||||||
|
case "deployments", "deployment":
|
||||||
|
return fmt.Sprintf("/apis/apps/v1/namespaces/%s/deployments/%s", ns, name), nil
|
||||||
|
case "statefulsets", "statefulset":
|
||||||
|
return fmt.Sprintf("/apis/apps/v1/namespaces/%s/statefulsets/%s", ns, name), nil
|
||||||
|
case "daemonsets", "daemonset":
|
||||||
|
return fmt.Sprintf("/apis/apps/v1/namespaces/%s/daemonsets/%s", ns, name), nil
|
||||||
|
case "replicasets", "replicaset":
|
||||||
|
return fmt.Sprintf("/apis/apps/v1/namespaces/%s/replicasets/%s", ns, name), nil
|
||||||
|
case "pods", "pod":
|
||||||
|
return fmt.Sprintf("/api/v1/namespaces/%s/pods/%s", ns, name), nil
|
||||||
|
case "services", "service":
|
||||||
|
return fmt.Sprintf("/api/v1/namespaces/%s/services/%s", ns, name), nil
|
||||||
|
case "ingresses", "ingress":
|
||||||
|
return fmt.Sprintf("/apis/networking.k8s.io/v1/namespaces/%s/ingresses/%s", ns, name), nil
|
||||||
|
case "configmaps", "configmap":
|
||||||
|
return fmt.Sprintf("/api/v1/namespaces/%s/configmaps/%s", ns, name), nil
|
||||||
|
case "secrets", "secret":
|
||||||
|
return fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s", ns, name), nil
|
||||||
|
case "pvcs", "persistentvolumeclaim":
|
||||||
|
return fmt.Sprintf("/api/v1/namespaces/%s/persistentvolumeclaims/%s", ns, name), nil
|
||||||
|
case "hpas", "hpa", "horizontalpodautoscaler":
|
||||||
|
return fmt.Sprintf("/apis/autoscaling/v2/namespaces/%s/horizontalpodautoscalers/%s", ns, name), nil
|
||||||
|
case "cronjobs", "cronjob":
|
||||||
|
return fmt.Sprintf("/apis/batch/v1/namespaces/%s/cronjobs/%s", ns, name), nil
|
||||||
|
case "serviceaccounts", "serviceaccount":
|
||||||
|
return fmt.Sprintf("/api/v1/namespaces/%s/serviceaccounts/%s", ns, name), nil
|
||||||
|
case "roles", "role":
|
||||||
|
return fmt.Sprintf("/apis/rbac.authorization.k8s.io/v1/namespaces/%s/roles/%s", ns, name), nil
|
||||||
|
case "rolebindings", "rolebinding":
|
||||||
|
return fmt.Sprintf("/apis/rbac.authorization.k8s.io/v1/namespaces/%s/rolebindings/%s", ns, name), nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unsupported resource type: %s", resource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteResourceYAMLNS deletes a K8s resource (Service/Ingress/ConfigMap etc.)
|
||||||
|
func (h *KubernetesHandler) DeleteResourceYAMLNS(c *gin.Context) {
|
||||||
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
ns := c.Param("namespace")
|
||||||
|
resource := c.Param("resource")
|
||||||
|
name := c.Param("name")
|
||||||
|
apiPath, err := resourceAPIPath(ns, resource, name)
|
||||||
|
if err != nil { c.JSON(400, models.Response{Code: 400, Message: err.Error()}); return }
|
||||||
|
if err := client.DeleteResource(apiPath); err != nil {
|
||||||
|
c.JSON(500, models.Response{Code: 500, Message: "删除失败: " + err.Error()}); return
|
||||||
|
}
|
||||||
|
c.JSON(200, models.Response{Code: 200, Message: "资源已删除"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// getK8sClient builds a K8s API client from cluster's stored kubeconfig
|
||||||
|
func (h *KubernetesHandler) ApplyYAML(c *gin.Context) {
|
||||||
|
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
|
||||||
|
if clusterID == 0 { c.JSON(500, models.Response{Code: 500, Message: "cluster not found"}); return }
|
||||||
|
client, _, err := h.getK8sClient(clusterID)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: err.Error()}); return }
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
YAML string `json:"yaml"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, models.Response{Code: 400, Message: "参数错误: " + err.Error()}); return }
|
||||||
|
if req.YAML == "" { c.JSON(400, models.Response{Code: 400, Message: "YAML 内容不能为空"}); return }
|
||||||
|
|
||||||
|
result, err := client.ApplyYAML(req.YAML)
|
||||||
|
if err != nil { c.JSON(500, models.Response{Code: 500, Message: "Apply 失败: " + err.Error()}); return }
|
||||||
|
|
||||||
|
c.JSON(200, models.Response{Code: 200, Message: "资源已应用", Data: result})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ReplicaSet 列表 / Deployment 回滚 / 历史版本 // Logan.Li
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// ListReplicaSetsNS 列出 namespace 下的 ReplicaSet
|
||||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user