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
@@ -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 聚合所有集群的资源数据进行对比
|
||||
Reference in new issue
Block a user