Files
aiops/backend/controller/k8s_pod_files.go
T
attacker 77c234b060 feat(audit): F1+F2 告警静默规则标记完成 + 审计日志CSV导出实现
F1 告警静默规则 [done]:
- 后端 alert_silence.go: CRUD + Toggle (onetime/daily/weekly 三模式)
- 告警引擎 isSilenced 集成 (alert_engine.go)
- 前端 SilenceRuleTab + SilenceDialog
- 路由 /alerts/silences 已注册

F2 审计日志导出 [done]:
- 重写 ExportAuditLogs: 真正的 CSV 导出 (encoding/csv + UTF-8 BOM)
- 支持 module/action/username/startTime/endTime 筛选
- 导出限制 10000 条防溢出,自动记录审计日志
- JSON 格式向后兼容 (format=json)
- 前端 exportLogs 传递查询参数

同时包含累积的前期工作:
- 目录重构 internal/ -> controller/model/router (v0.55-v0.57)
- 后端分层架构 dto/repository/service/utils
- 审计模块全功能 (终端审计/操作审计/登录日志)
- asciicast v2 录制格式
- SFTP 文件浏览器
- StatStrip 紧凑统计条
- 在线会话自动刷新
- LiveMonitor WebSocket 诊断修复

验证: go build ✅ | npm run build ✅ | Docker 部署 ✅ | 10 回归端点 ✅ | CSV 导出 ✅ | 静默 CRUD ✅
2026-06-25 20:16:30 +08:00

207 lines
7.1 KiB
Go

package controller
import (
"encoding/base64"
"fmt"
"io"
"log"
"path"
"strconv"
"strings"
"ezops/model"
"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, model.Response{Code: 400, Message: "参数错误: " + err.Error()}); return }
if req.Path == "" { req.Path = "/" }
client, _, err := h.getK8sClient(clusterID)
if err != nil { c.JSON(500, model.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, model.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, model.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, model.Response{Code: 400, Message: "path 参数不能为空"}); return }
client, _, err := h.getK8sClient(clusterID)
if err != nil { c.JSON(500, model.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, model.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, model.Response{Code: 400, Message: "文件上传失败: " + err.Error()})
return
}
defer file.Close()
// 限制文件大小 10MB
const maxUploadSize = 10 << 20 // 10MB
if header.Size > maxUploadSize {
c.JSON(400, model.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, model.Response{Code: 500, Message: "读取文件失败: " + err.Error()})
return
}
client, _, err := h.getK8sClient(clusterID)
if err != nil {
c.JSON(500, model.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, model.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, model.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 聚合所有集群的资源数据进行对比