feat: K8s集群导出改为导出所有命名空间资源,去掉命名空间选择

- 后端: 新增 ExportResourcesAllNS handler (GET /:clusterCode/export),遍历所有NS收集资源
- 前端: KubernetesClusters 导出弹窗去掉命名空间输入框,一键导出全部NS
- API: exportResources → exportAllResources,无需传 namespace 参数
This commit is contained in:
attacker committed 2026-06-23 01:57:05 +00:00
1 parent a54b2d86b0
commit dd7cb6895e
4 files changed
+159 -23

No files matched your search

+152 -6
View File
@@ -16,9 +16,122 @@ import (
// yamlEntry 表示一个待打包的 YAML 资源
type yamlEntry struct {
folder string // tar 内子目录
name string // 文件名(不含 .yaml)
yaml []byte // 清理后的 YAML
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
@@ -212,15 +325,48 @@ func cleanRuntimeAnnotations(annotations map[string]interface{}) {
}
}
// buildTarGz 构建 tar.gz 包
// 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 {
// 路径: {namespace}/{folder}/{name}.yaml
path := fmt.Sprintf("%s/%s/%s.yaml", namespace, e.folder, e.name)
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,
+1
View File
@@ -398,6 +398,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
// 🆕 YAML Apply
kubernetesRouter.POST("/:clusterCode/yaml/apply", k8sHandler.ApplyYAML)
// 🆕 批量导出资源 (Deployment+Service+Ingress+ConfigMap+Secret) // Logan.Li
kubernetesRouter.GET("/:clusterCode/export", k8sHandler.ExportResourcesAllNS)
kubernetesRouter.GET("/:clusterCode/namespace/:namespace/export", k8sHandler.ExportResourcesNS)
// PVC (存储)