Files
aiops/backend/internal/api/handlers/k8s_export.go
T
attacker dd7cb6895e feat: K8s集群导出改为导出所有命名空间资源,去掉命名空间选择
- 后端: 新增 ExportResourcesAllNS handler (GET /:clusterCode/export),遍历所有NS收集资源
- 前端: KubernetesClusters 导出弹窗去掉命名空间输入框,一键导出全部NS
- API: exportResources → exportAllResources,无需传 namespace 参数
2026-06-23 01:57:05 +00:00

391 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 {
namespace string // 所属命名空间(用于 tar 内路径)
folder string // tar 内子目录
name string // 文件名(不含 .yaml)
yaml []byte // 清理后的 YAML
}
// ExportResourcesAllNS 导出集群所有命名空间的 Deployment+Service+Ingress+ConfigMap+Secret 为 tar.gz
func (h *KubernetesHandler) ExportResourcesAllNS(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
}
// 获取所有命名空间
namespaces, err := client.ListNamespaces()
if err != nil {
c.JSON(500, gin.H{"code": 500, "message": "list namespaces failed: " + err.Error()})
return
}
// 资源类型 → API 路径模板 + 子目录名
type resourceDef struct {
apiPathTmpl string // %s = namespace
folder string
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"},
}
var entries []yamlEntry
totalCount := 0
for _, ns := range namespaces {
for _, rd := range resourceDefs {
listPath := fmt.Sprintf(rd.apiPathTmpl, ns)
yamlBytes, err := client.GetResourceYAML(listPath)
if err != nil {
log.Printf("[ExportAllNS] %s/%s list failed: %v", ns, rd.folder, err)
continue
}
var listObj map[string]interface{}
if err := yaml.Unmarshal(yamlBytes, &listObj); err != nil {
log.Printf("[ExportAllNS] %s/%s parse failed: %v", ns, 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
}
itemMap["apiVersion"] = rd.apiVersion
itemMap["kind"] = rd.kind
cleanResourceYAML(itemMap)
cleanedBytes, err := yaml.Marshal(itemMap)
if err != nil {
log.Printf("[ExportAllNS] marshal %s/%s/%s failed: %v", ns, rd.folder, name, err)
continue
}
entries = append(entries, yamlEntry{
namespace: ns,
folder: rd.folder,
name: name,
yaml: cleanedBytes,
})
totalCount++
}
}
}
if totalCount == 0 {
c.JSON(404, gin.H{"code": 404, "message": "集群没有可导出的资源"})
return
}
tarGz, err := buildTarGzAllNS(entries)
if err != nil {
c.JSON(500, gin.H{"code": 500, "message": "打包失败: " + err.Error()})
return
}
clusterName := c.Param("clusterCode")
filename := fmt.Sprintf("cluster-%s-%s.tar.gz", clusterName, 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("[ExportAllNS] exported %d resources across %d namespaces (cluster %d)", totalCount, len(namespaces), clusterID)
}
// 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 包(单命名空间,路径: ns/folder/name.yaml)
func buildTarGz(entries []yamlEntry, namespace string) ([]byte, error) {
var buf bytes.Buffer
gw := gzip.NewWriter(&buf)
tw := tar.NewWriter(gw)
for _, e := range entries {
ns := e.namespace
if ns == "" {
ns = namespace
}
path := fmt.Sprintf("%s/%s/%s.yaml", ns, 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
}
// buildTarGzAllNS 构建 tar.gz 包(多命名空间,路径: ns/folder/name.yaml)
func buildTarGzAllNS(entries []yamlEntry) ([]byte, error) {
var buf bytes.Buffer
gw := gzip.NewWriter(&buf)
tw := tar.NewWriter(gw)
for _, e := range entries {
path := fmt.Sprintf("%s/%s/%s.yaml", e.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
}