fix: YAML查看器按钮文案 '清理并编辑' → '编辑',统一简洁命名

This commit is contained in:
attacker committed 2026-06-23 01:23:27 +00:00
1 parent a8b4af4409
commit 4bd9fa629b
14 files changed
+2146 -400

No files matched your search

+244
View File
@@ -0,0 +1,244 @@
package handlers
import (
"archive/tar"
"bytes"
"compress/gzip"
"fmt"
"io"
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
"gopkg.in/yaml.v3"
)
// yamlEntry 表示一个待打包的 YAML 资源
type yamlEntry struct {
folder string // tar 内子目录
name string // 文件名(不含 .yaml)
yaml []byte // 清理后的 YAML
}
// ExportResourcesNS 导出指定命名空间的 Deployment + Service + Ingress + ConfigMap + Secret 为 tar.gz
func (h *KubernetesHandler) ExportResourcesNS(c *gin.Context) {
clusterID := h.resolveClusterCode(c.Param("clusterCode"))
if clusterID == 0 {
c.JSON(500, gin.H{"code": 500, "message": "cluster not found"})
return
}
client, _, err := h.getK8sClient(clusterID)
if err != nil {
c.JSON(500, gin.H{"code": 500, "message": err.Error()})
return
}
ns := c.Param("namespace")
if ns == "" {
c.JSON(400, gin.H{"code": 400, "message": "namespace is required"})
return
}
// 资源类型 → API 路径模板 + 子目录名
type resourceDef struct {
apiPathTmpl string // %s = namespace
folder string // tar 内子目录
kind string
apiVersion string
}
resourceDefs := []resourceDef{
{"/apis/apps/v1/namespaces/%s/deployments", "deployments", "Deployment", "apps/v1"},
{"/api/v1/namespaces/%s/services", "services", "Service", "v1"},
{"/apis/networking.k8s.io/v1/namespaces/%s/ingresses", "ingresses", "Ingress", "networking.k8s.io/v1"},
{"/api/v1/namespaces/%s/configmaps", "configmaps", "ConfigMap", "v1"},
{"/api/v1/namespaces/%s/secrets", "secrets", "Secret", "v1"},
}
// 收集所有资源的 YAML
var entries []yamlEntry
totalCount := 0
for _, rd := range resourceDefs {
listPath := fmt.Sprintf(rd.apiPathTmpl, ns)
yamlBytes, err := client.GetResourceYAML(listPath)
if err != nil {
log.Printf("[ExportResources] list %s failed: %v", rd.folder, err)
continue
}
// 解析 YAML list(K8s List 返回 kind: XxxList)
var listObj map[string]interface{}
if err := yaml.Unmarshal(yamlBytes, &listObj); err != nil {
log.Printf("[ExportResources] parse %s list failed: %v", rd.folder, err)
continue
}
items, ok := listObj["items"].([]interface{})
if !ok || len(items) == 0 {
continue
}
for _, item := range items {
itemMap, ok := item.(map[string]interface{})
if !ok {
continue
}
// 获取资源名
meta, _ := itemMap["metadata"].(map[string]interface{})
name, _ := meta["name"].(string)
if name == "" {
continue
}
// 设置 apiVersion 和 kind
itemMap["apiVersion"] = rd.apiVersion
itemMap["kind"] = rd.kind
// 清理运行时字段
cleanResourceYAML(itemMap)
// 序列化为 YAML
cleanedBytes, err := yaml.Marshal(itemMap)
if err != nil {
log.Printf("[ExportResources] marshal %s/%s failed: %v", rd.folder, name, err)
continue
}
entries = append(entries, yamlEntry{
folder: rd.folder,
name: name,
yaml: cleanedBytes,
})
totalCount++
}
}
if totalCount == 0 {
c.JSON(404, gin.H{"code": 404, "message": "命名空间 " + ns + " 没有可导出的资源"})
return
}
// 构建 tar.gz
tarGz, err := buildTarGz(entries, ns)
if err != nil {
c.JSON(500, gin.H{"code": 500, "message": "打包失败: " + err.Error()})
return
}
filename := fmt.Sprintf("%s-%s.tar.gz", ns, time.Now().Format("20060102-150405"))
c.Header("Content-Type", "application/gzip")
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
c.Header("Content-Length", fmt.Sprintf("%d", len(tarGz)))
c.Data(http.StatusOK, "application/gzip", tarGz)
log.Printf("[ExportResources] exported %d resources from namespace %s (cluster %d)", totalCount, ns, clusterID)
}
// cleanResourceYAML 清理 K8s 资源中的运行时字段(通用,适用于所有资源类型)
func cleanResourceYAML(obj map[string]interface{}) {
// 清理 metadata
if meta, ok := obj["metadata"].(map[string]interface{}); ok {
delete(meta, "managedFields")
delete(meta, "creationTimestamp")
delete(meta, "resourceVersion")
delete(meta, "uid")
delete(meta, "selfLink")
delete(meta, "generation")
delete(meta, "ownerReferences")
delete(meta, "finalizers")
delete(meta, "generateName")
// 清理运行时 annotations
if annotations, ok := meta["annotations"].(map[string]interface{}); ok {
cleanRuntimeAnnotations(annotations)
if len(annotations) == 0 {
delete(meta, "annotations")
}
}
}
// 删除 status
delete(obj, "status")
// 清理 spec 中的运行时字段
if spec, ok := obj["spec"].(map[string]interface{}); ok {
// Service: 移除动态分配的 clusterIP 和 nodePorts
if obj["kind"] == "Service" {
delete(spec, "clusterIP")
delete(spec, "clusterIPs")
// nodePorts 在 ports 数组内 — 仅移除 NodePort 类型的 nodePort 字段
if ports, ok := spec["ports"].([]interface{}); ok {
for _, p := range ports {
if pm, ok := p.(map[string]interface{}); ok {
delete(pm, "nodePort")
}
}
}
}
// Deployment: 清理 template.metadata 和 template.spec 中的运行时字段
if obj["kind"] == "Deployment" {
if template, ok := spec["template"].(map[string]interface{}); ok {
if tMeta, ok := template["metadata"].(map[string]interface{}); ok {
delete(tMeta, "creationTimestamp")
if tAnn, ok := tMeta["annotations"].(map[string]interface{}); ok {
cleanRuntimeAnnotations(tAnn)
if len(tAnn) == 0 {
delete(tMeta, "annotations")
}
}
}
if tSpec, ok := template["spec"].(map[string]interface{}); ok {
delete(tSpec, "nodeName")
}
}
}
}
}
// cleanRuntimeAnnotations 移除 K8s 系统/运行时 annotations
func cleanRuntimeAnnotations(annotations map[string]interface{}) {
runtimeKeys := []string{
"kubectl.kubernetes.io/last-applied-configuration",
"deployment.kubernetes.io/revision",
"control-plane.alpha.kubernetes.io/leader",
"autoscaling.alpha.kubernetes.io/conditions",
"pv.kubernetes.io/bind-completed",
"pv.kubernetes.io/bound-by-controller",
}
for _, k := range runtimeKeys {
delete(annotations, k)
}
}
// buildTarGz 构建 tar.gz 包
func buildTarGz(entries []yamlEntry, namespace string) ([]byte, error) {
var buf bytes.Buffer
gw := gzip.NewWriter(&buf)
tw := tar.NewWriter(gw)
for _, e := range entries {
// 路径: {namespace}/{folder}/{name}.yaml
path := fmt.Sprintf("%s/%s/%s.yaml", namespace, e.folder, e.name)
header := &tar.Header{
Name: path,
Mode: 0644,
Size: int64(len(e.yaml)),
}
if err := tw.WriteHeader(header); err != nil {
return nil, fmt.Errorf("tar write header %s: %w", path, err)
}
if _, err := io.Copy(tw, bytes.NewReader(e.yaml)); err != nil {
return nil, fmt.Errorf("tar write body %s: %w", path, err)
}
}
if err := tw.Close(); err != nil {
return nil, fmt.Errorf("tar close: %w", err)
}
if err := gw.Close(); err != nil {
return nil, fmt.Errorf("gzip close: %w", err)
}
return buf.Bytes(), nil
}