Files
aiops/backend/internal/api/handlers/k8s_export.go
T
attacker 451f65f9c8 feat: 路由改名/kubectl预装/导出YAML精简/Service标签页优化/Alpine镜像源换清华
- 路由 /deploy/credentials → /deploy/configureTools
- kubectl 改为预装(Dockerfile) + 种子数据 preinstall
- 导出YAML精简: Service/Ingress/Deployment 全面清理运行时字段
- DeploymentEditService 只显示匹配 selector 的关联 Service
- Alpine 镜像源 mirrors.aliyun.com → mirrors.tuna.tsinghua.edu.cn
2026-06-23 02:22:52 +00:00

452 lines
13 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")
}
}
// 移除空的 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
}