fix: K8s 集群添加缺少 code 必填字段
- KubernetesClusterDialog: 新增 code 表单字段 - 名称变更自动生成 code(小写+连字符) - handleSubmit 增加 code 校验 + toast 提示 - payload 增加 code 字段(后端 binding:required)
This commit is contained in:
1 parent
f24fe43ac0
commit
ce438c9f05
18 files changed
+3941
-144
No files matched your search
@@ -6,6 +6,8 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -1723,13 +1725,25 @@ func (h *KubernetesHandler) getPodLogsWithNS(c *gin.Context, ns string) {
|
||||
clusterID, _ := strconv.Atoi(c.Param("id"))
|
||||
if clusterID == 0 { clusterID = h.resolveClusterCode(c.Param("clusterCode")) }
|
||||
podName := c.Param("podName")
|
||||
container := c.Query("container")
|
||||
tailLines, _ := strconv.Atoi(c.DefaultQuery("tail", "200"))
|
||||
stream := c.Query("stream") == "true"
|
||||
// 日志增强参数: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.GetPodLogsStream(ns, podName, container, tailLines)
|
||||
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")
|
||||
@@ -1741,9 +1755,9 @@ func (h *KubernetesHandler) getPodLogsWithNS(c *gin.Context, ns string) {
|
||||
})
|
||||
return
|
||||
}
|
||||
logs, err := client.GetPodLogs(ns, podName, container, tailLines)
|
||||
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, Data: gin.H{"logs": logs, "pod": podName, "namespace": ns}})
|
||||
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"))
|
||||
@@ -1938,20 +1952,30 @@ func (h *KubernetesHandler) GetResourceYAMLNS(c *gin.Context) {
|
||||
// Build API path based on resource type
|
||||
var apiPath string
|
||||
switch resource {
|
||||
case "deployments":
|
||||
case "deployments", "deployment":
|
||||
apiPath = fmt.Sprintf("/apis/apps/v1/namespaces/%s/deployments/%s", ns, name)
|
||||
case "pods":
|
||||
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":
|
||||
case "services", "service":
|
||||
apiPath = fmt.Sprintf("/api/v1/namespaces/%s/services/%s", ns, name)
|
||||
case "ingresses":
|
||||
case "ingresses", "ingress":
|
||||
apiPath = fmt.Sprintf("/apis/networking.k8s.io/v1/namespaces/%s/ingresses/%s", ns, name)
|
||||
case "configmaps":
|
||||
case "configmaps", "configmap":
|
||||
apiPath = fmt.Sprintf("/api/v1/namespaces/%s/configmaps/%s", ns, name)
|
||||
case "secrets":
|
||||
case "secrets", "secret":
|
||||
apiPath = fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s", ns, name)
|
||||
case "pvcs":
|
||||
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)
|
||||
default:
|
||||
c.JSON(400, models.Response{Code: 400, Message: "unsupported resource type: " + resource}); return
|
||||
}
|
||||
@@ -1974,12 +1998,30 @@ func (h *KubernetesHandler) UpdateResourceYAMLNS(c *gin.Context) {
|
||||
if err != nil { c.JSON(400, models.Response{Code: 400, Message: "读取body失败"}); return }
|
||||
var apiPath string
|
||||
switch resource {
|
||||
case "deployments":
|
||||
case "deployments", "deployment":
|
||||
apiPath = fmt.Sprintf("/apis/apps/v1/namespaces/%s/deployments/%s", ns, name)
|
||||
case "pods":
|
||||
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":
|
||||
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)
|
||||
default:
|
||||
c.JSON(400, models.Response{Code: 400, Message: "unsupported resource type: " + resource}); return
|
||||
}
|
||||
@@ -2268,3 +2310,274 @@ func (h *KubernetesHandler) ApplyYAML(c *gin.Context) {
|
||||
|
||||
c.JSON(200, models.Response{Code: 200, Message: "资源已应用", Data: result})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ReplicaSet 列表 / Deployment 回滚 / 历史版本 // Logan.Li
|
||||
// ============================================================================
|
||||
|
||||
// ListReplicaSetsNS 列出 namespace 下的 ReplicaSet
|
||||
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
|
||||
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 内指定路径的文件
|
||||
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 配合自定义 time-style 获取清晰可解析的字段
|
||||
cmd := []string{"ls", "-la", "--time-style=+%Y-%m-%dT%H:%M:%S", 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 := cleanExecOutput(result.Stdout)
|
||||
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]
|
||||
modTime := fields[5]
|
||||
// 文件名可能含空格:定位到 modTime 之后的内容作为 name
|
||||
name := ""
|
||||
if idx := strings.Index(line, modTime); idx >= 0 {
|
||||
name = strings.TrimSpace(line[idx+len(modTime):])
|
||||
}
|
||||
if name == "" { name = strings.Join(fields[6:], " ") }
|
||||
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 := cleanExecOutput(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)
|
||||
}
|
||||
@@ -306,6 +306,14 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
||||
clusters.POST("/:id/namespaces/:namespace/cronjobs/:name/suspend", k8sHandler.SuspendCronJob)
|
||||
clusters.POST("/:id/namespaces/:namespace/cronjobs/:name/trigger", k8sHandler.TriggerCronJob)
|
||||
clusters.DELETE("/:id/namespaces/:namespace/cronjobs/:name", k8sHandler.DeleteCronJob)
|
||||
// ReplicaSet 列表 / Deployment 回滚 / 历史版本 // Logan.Li
|
||||
clusters.GET("/:id/namespaces/:namespace/replicasets", k8sHandler.ListReplicaSetsNS)
|
||||
clusters.GET("/:id/namespaces/:namespace/deployments/:name/revisions", k8sHandler.ListDeploymentRevisionsNS)
|
||||
clusters.POST("/:id/namespaces/:namespace/deployments/:name/rollback", k8sHandler.RollbackDeploymentNS)
|
||||
// Pod 文件浏览 / 下载 / 删除 // Logan.Li
|
||||
clusters.POST("/:id/namespaces/:namespace/pods/:podName/files/list", k8sHandler.ListPodFilesNS)
|
||||
clusters.GET("/:id/namespaces/:namespace/pods/:podName/files/download", k8sHandler.DownloadPodFileNS)
|
||||
clusters.DELETE("/:id/namespaces/:namespace/pods/:podName", k8sHandler.DeletePodNS)
|
||||
// Namespace // Logan.Li
|
||||
clusters.GET("/:id/namespaces", k8sHandler.ListNamespaces)
|
||||
}
|
||||
@@ -349,6 +357,16 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
|
||||
kubernetesRouter.POST("/:clusterCode/namespace/:namespace/cronjobs/:name/trigger", k8sHandler.TriggerCronJobNS)
|
||||
kubernetesRouter.DELETE("/:clusterCode/namespace/:namespace/cronjobs/:name", k8sHandler.DeleteCronJobNS)
|
||||
|
||||
// 🆕 ReplicaSet 列表 // Logan.Li
|
||||
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/replicasets", k8sHandler.ListReplicaSetsNS)
|
||||
// 🆕 Deployment 回滚 / 发布历史 // Logan.Li
|
||||
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/deployments/:name/revisions", k8sHandler.ListDeploymentRevisionsNS)
|
||||
kubernetesRouter.POST("/:clusterCode/namespace/:namespace/deployments/:name/rollback", k8sHandler.RollbackDeploymentNS)
|
||||
// 🆕 Pod 文件浏览 / 下载 / 删除 // Logan.Li
|
||||
kubernetesRouter.POST("/:clusterCode/namespace/:namespace/pods/:podName/files/list", k8sHandler.ListPodFilesNS)
|
||||
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/pods/:podName/files/download", k8sHandler.DownloadPodFileNS)
|
||||
kubernetesRouter.DELETE("/:clusterCode/namespace/:namespace/pods/:podName", k8sHandler.DeletePodNS)
|
||||
|
||||
// ConfigMap / Secret
|
||||
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/configmaps", k8sHandler.ListConfigMaps)
|
||||
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/secrets", k8sHandler.ListSecrets)
|
||||
|
||||
@@ -1316,3 +1316,171 @@ func (c *Client) resourceAPIPath(apiVersion, kind, namespace, name string) (stri
|
||||
}
|
||||
return "/api/v1/namespaces/" + ns + "/" + resource + "/" + name, true
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ReplicaSet — 用于 Deployment 回滚 / 历史版本查询 // Logan.Li
|
||||
// ============================================================================
|
||||
|
||||
// ReplicaSetList response
|
||||
type ReplicaSetList struct {
|
||||
Items []ReplicaSetItem `json:"items"`
|
||||
}
|
||||
|
||||
// ReplicaSetItem 表示一个 ReplicaSet(含完整 spec/status/metadata,用于回滚取 template)
|
||||
type ReplicaSetItem struct {
|
||||
Metadata ObjectMeta `json:"metadata"`
|
||||
Spec ReplicaSetSpec `json:"spec"`
|
||||
Status ReplicaSetStatus `json:"status"`
|
||||
}
|
||||
|
||||
// ReplicaSetSpec 与 DeploymentSpec 结构一致,关键是 template
|
||||
type ReplicaSetSpec struct {
|
||||
Replicas int `json:"replicas"`
|
||||
Template ReplicaSetTemplate `json:"template"`
|
||||
}
|
||||
|
||||
// ReplicaSetTemplate pod 模板
|
||||
type ReplicaSetTemplate struct {
|
||||
Metadata ObjectMeta `json:"metadata"`
|
||||
Spec PodSpec `json:"spec"`
|
||||
}
|
||||
|
||||
// ReplicaSetStatus 副本状态
|
||||
type ReplicaSetStatus struct {
|
||||
Replicas int `json:"replicas"`
|
||||
AvailableReplicas int `json:"availableReplicas"`
|
||||
ReadyReplicas int `json:"readyReplicas"`
|
||||
}
|
||||
|
||||
// OwnerReference 资源归属引用
|
||||
type OwnerReference struct {
|
||||
Kind string `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// ReplicaSetOwnerMeta 在 ListReplicaSets 时额外携带 ownerReferences
|
||||
type ReplicaSetOwnerMeta struct {
|
||||
Metadata struct {
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
Annotations map[string]string `json:"annotations"`
|
||||
CreationTimestamp string `json:"creationTimestamp"`
|
||||
OwnerReferences []OwnerReference `json:"ownerReferences"`
|
||||
} `json:"metadata"`
|
||||
Spec struct {
|
||||
Replicas int `json:"replicas"`
|
||||
Template ReplicaSetTemplate `json:"template"`
|
||||
} `json:"spec"`
|
||||
Status ReplicaSetStatus `json:"status"`
|
||||
}
|
||||
|
||||
// ListReplicaSetsRaw 列出 namespace 下的 ReplicaSet(含 ownerReferences 原始字段)
|
||||
func (c *Client) ListReplicaSetsRaw(namespace string) ([]ReplicaSetOwnerMeta, error) {
|
||||
var result struct {
|
||||
Items []ReplicaSetOwnerMeta `json:"items"`
|
||||
}
|
||||
path := fmt.Sprintf("/apis/apps/v1/namespaces/%s/replicasets", namespace)
|
||||
if err := c.get(path, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Items, nil
|
||||
}
|
||||
|
||||
// GetReplicaSet 获取单个 ReplicaSet 的完整 JSON(map 形式,便于取 template 直接 patch)
|
||||
func (c *Client) GetReplicaSet(namespace, name string) (map[string]interface{}, error) {
|
||||
path := fmt.Sprintf("/apis/apps/v1/namespaces/%s/replicasets/%s", namespace, name)
|
||||
var result map[string]interface{}
|
||||
if err := c.get(path, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// PatchDeploymentSpecTemplate 用旧 ReplicaSet 的 template 回滚 Deployment
|
||||
// 通过 merge-patch 将 deployment.spec.template 整体替换为 rsTemplate
|
||||
func (c *Client) PatchDeploymentSpecTemplate(namespace, deploymentName string, rsTemplate map[string]interface{}) error {
|
||||
body := map[string]interface{}{
|
||||
"spec": map[string]interface{}{
|
||||
"template": rsTemplate,
|
||||
},
|
||||
}
|
||||
path := fmt.Sprintf("/apis/apps/v1/namespaces/%s/deployments/%s", namespace, deploymentName)
|
||||
return c.patch(path, body, nil)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Pod 删除 / 高级日志参数 // Logan.Li
|
||||
// ============================================================================
|
||||
|
||||
// DeletePod 删除指定 namespace 下的 Pod
|
||||
func (c *Client) DeletePod(namespace, name string) error {
|
||||
path := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s", namespace, name)
|
||||
return c.delete(path)
|
||||
}
|
||||
|
||||
// LogOptions 日志查询高级参数(container/sinceTime/previous/tailLines)
|
||||
type LogOptions struct {
|
||||
Container string
|
||||
SinceTime string // RFC3339 格式,仅返回此时间之后的日志
|
||||
Previous bool // 返回已终止容器(previous instance)的日志
|
||||
TailLines int // 末尾 N 行,0 表示不限制
|
||||
}
|
||||
|
||||
// buildLogPath 构造 K8s pod log API 的完整查询路径
|
||||
func (c *Client) buildLogPath(namespace, podName string, opts LogOptions, follow bool) string {
|
||||
q := []string{}
|
||||
if opts.Container != "" {
|
||||
q = append(q, "container="+opts.Container)
|
||||
}
|
||||
if opts.TailLines > 0 {
|
||||
q = append(q, fmt.Sprintf("tailLines=%d", opts.TailLines))
|
||||
}
|
||||
if opts.SinceTime != "" {
|
||||
q = append(q, "sinceTime="+opts.SinceTime)
|
||||
}
|
||||
if opts.Previous {
|
||||
q = append(q, "previous=true")
|
||||
}
|
||||
if follow {
|
||||
q = append(q, "follow=true")
|
||||
}
|
||||
path := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/log", namespace, podName)
|
||||
if len(q) > 0 {
|
||||
path += "?" + strings.Join(q, "&")
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// GetPodLogsWithOptions 支持高级参数的日志查询(非流式)
|
||||
func (c *Client) GetPodLogsWithOptions(namespace, podName string, opts LogOptions) (string, error) {
|
||||
url := c.BaseURL + c.buildLogPath(namespace, podName, opts, false)
|
||||
resp, err := c.HTTPClient.Get(url)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("GET logs: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
return "", fmt.Errorf("K8s API %d: %s", resp.StatusCode, string(body[:min(len(body), 300)]))
|
||||
}
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
// GetPodLogsStreamWithOptions 支持高级参数的日志流式查询(follow=true)
|
||||
func (c *Client) GetPodLogsStreamWithOptions(namespace, podName string, opts LogOptions) (io.ReadCloser, error) {
|
||||
url := c.BaseURL + c.buildLogPath(namespace, podName, opts, true)
|
||||
resp, err := c.HTTPClient.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GET logs stream: %w", err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
return nil, fmt.Errorf("K8s API %d: %s", resp.StatusCode, string(body[:min(len(body), 300)]))
|
||||
}
|
||||
return resp.Body, nil
|
||||
}
|
||||
Reference in new issue
Block a user