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") } } // 移除空的 labels if labels, ok := meta["labels"].(map[string]interface{}); ok && len(labels) == 0 { delete(meta, "labels") } } // 删除 status delete(obj, "status") // 清理 spec 中的运行时字段 if spec, ok := obj["spec"].(map[string]interface{}); ok { kind, _ := obj["kind"].(string) // Service: 移除动态分配字段和默认值 if kind == "Service" { delete(spec, "clusterIP") delete(spec, "clusterIPs") delete(spec, "internalTrafficPolicy") delete(spec, "ipFamilies") delete(spec, "ipFamilyPolicy") // sessionAffinity 默认 "None" → 移除 if sa, ok := spec["sessionAffinity"].(string); ok && sa == "None" { delete(spec, "sessionAffinity") } delete(spec, "allocateLoadBalancerNodePorts") // 移除 ports 内的 nodePort(仅 NodePort/LoadBalancer 类型) if ports, ok := spec["ports"].([]interface{}); ok { for _, p := range ports { if pm, ok := p.(map[string]interface{}); ok { delete(pm, "nodePort") } } } } // Deployment / StatefulSet / DaemonSet: 清理运行时和默认值 if kind == "Deployment" || kind == "StatefulSet" || kind == "DaemonSet" { // 清理 template 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") delete(tSpec, "serviceAccountName") // 移除 default 值 // 清理容器内的 terminationMessagePath/Policy 默认值 if containers, ok := tSpec["containers"].([]interface{}); ok { for _, c := range containers { if cm, ok := c.(map[string]interface{}); ok { if tmp, ok := cm["terminationMessagePath"].(string); ok && tmp == "/dev/termination-log" { delete(cm, "terminationMessagePath") } if tmp, ok := cm["terminationMessagePolicy"].(string); ok && tmp == "File" { delete(cm, "terminationMessagePolicy") } // 清理默认 imagePullPolicy if ip, ok := cm["imagePullPolicy"].(string); ok && ip == "Always" { delete(cm, "imagePullPolicy") } // 清理空 resources if res, ok := cm["resources"].(map[string]interface{}); ok && len(res) == 0 { delete(cm, "resources") } } } } // 清理 dnsPolicy 默认值 if dp, ok := tSpec["dnsPolicy"].(string); ok && dp == "ClusterFirst" { delete(tSpec, "dnsPolicy") } if rp, ok := tSpec["restartPolicy"].(string); ok && rp == "Always" { delete(tSpec, "restartPolicy") } if tp, ok := tSpec["terminationGracePeriodSeconds"].(float64); ok && tp == 30 { delete(tSpec, "terminationGracePeriodSeconds") } } } // 清理默认值 if rhl, ok := spec["revisionHistoryLimit"].(float64); ok && rhl == 10 { delete(spec, "revisionHistoryLimit") } if pds, ok := spec["progressDeadlineSeconds"].(float64); ok && pds == 600 { delete(spec, "progressDeadlineSeconds") } } // Ingress: 清理默认值 if kind == "Ingress" { if ic, ok := spec["ingressClassName"].(string); ok && ic == "" { delete(spec, "ingressClassName") } } } } // 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 }