Files
aiops/backend/internal/api/handlers/k8s_export.go
T

245 lines
6.8 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 {
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
}