package services import ( "context" "fmt" "os" "path/filepath" "time" "lokideploy/config" "lokideploy/models" "lokideploy/repository" "lokideploy/utils" ) type ClusterService struct{} func NewClusterService() *ClusterService { return &ClusterService{} } func SaveKubeConfig(name, content string) (string, error) { dir := config.GetConfig().KubeDir if err := os.MkdirAll(dir, 0700); err != nil { return "", err } path := filepath.Join(dir, fmt.Sprintf("%s.yaml", name)) if err := os.WriteFile(path, []byte(content), 0600); err != nil { return "", err } return path, nil } // Create 创建集群。token 接入时自动构造等价 kubeconfig,统一存储,使部署模块同样可用。 func (s *ClusterService) Create(name, description, configType, apiServer, kubeconfig, token, contextName string) (*models.Cluster, error) { if configType == "" { configType = "kubeconfig" } finalKubeconfig := kubeconfig if configType == "token" { // 安全默认:不嵌入 CA、不跳过 TLS 验证,依赖系统 CA 池。 // 自签证书的集群请改用 kubeconfig 模式(其内含 CA 证书)。 finalKubeconfig = BuildKubeconfigFromToken(apiServer, token, "", false) } // 展示用 APIServer:token 模式取入参,kubeconfig 模式从内容提取 if apiServer == "" { apiServer = ExtractAPIServer(finalKubeconfig) } path, err := SaveKubeConfig(name, finalKubeconfig) if err != nil { return nil, fmt.Errorf("kubeconfig 保存失败: %w", err) } cluster := models.Cluster{ ID: repository.GetDB().NextID("clusters"), Name: name, Description: description, ConfigType: configType, APIServer: apiServer, KubeConfig: finalKubeconfig, KubeConfigPath: path, Context: contextName, Status: "pending", CreatedAt: time.Now(), UpdatedAt: time.Now(), } db := repository.GetDB() db.Lock() db.Clusters = append(db.Clusters, cluster) db.Unlock() db.RequestSave() go s.Validate(cluster.ID) return &cluster, nil } func (s *ClusterService) Get(id uint) (*models.Cluster, error) { db := repository.GetDB() db.RLock() defer db.RUnlock() for i := range db.Clusters { if db.Clusters[i].ID == id { c := db.Clusters[i] return &c, nil } } return nil, utils.ErrNotFound } func (s *ClusterService) List(page, pageSize int) ([]models.Cluster, int64, error) { db := repository.GetDB() db.RLock() defer db.RUnlock() total := int64(len(db.Clusters)) start := (page - 1) * pageSize if start < 0 || start >= len(db.Clusters) { return []models.Cluster{}, total, nil } end := start + pageSize if end > len(db.Clusters) { end = len(db.Clusters) } result := make([]models.Cluster, end-start) copy(result, db.Clusters[start:end]) return result, total, nil } func (s *ClusterService) Update(id uint, updates map[string]interface{}) error { db := repository.GetDB() db.Lock() defer db.Unlock() for i := range db.Clusters { if db.Clusters[i].ID == id { if v, ok := updates["name"].(string); ok && v != "" { db.Clusters[i].Name = v } if v, ok := updates["description"].(string); ok { db.Clusters[i].Description = v } if v, ok := updates["status"].(string); ok && v != "" { db.Clusters[i].Status = v } if v, ok := updates["version"].(string); ok { db.Clusters[i].Version = v } if v, ok := updates["nodesCount"].(int); ok { db.Clusters[i].NodesCount = v } if v, ok := updates["context"].(string); ok { db.Clusters[i].Context = v } if v, ok := updates["lastConnected"].(time.Time); ok { db.Clusters[i].LastConnected = v } // 接入方式与展示字段 if v, ok := updates["configType"].(string); ok && v != "" { db.Clusters[i].ConfigType = v } if v, ok := updates["apiServer"].(string); ok { db.Clusters[i].APIServer = v } // 凭据字段:空值不覆盖,避免编辑其它项时清空 if v, ok := updates["kubeconfig"].(string); ok && v != "" { db.Clusters[i].KubeConfig = v if db.Clusters[i].KubeConfigPath != "" { _ = os.WriteFile(db.Clusters[i].KubeConfigPath, []byte(v), 0600) } } db.Clusters[i].UpdatedAt = time.Now() db.RequestSave() return nil } } return utils.ErrNotFound } func (s *ClusterService) Delete(id uint) error { db := repository.GetDB() db.Lock() defer db.Unlock() for i, c := range db.Clusters { if c.ID == id { if c.KubeConfigPath != "" { _ = os.Remove(c.KubeConfigPath) } db.Clusters = append(db.Clusters[:i], db.Clusters[i+1:]...) db.RequestSave() return nil } } return utils.ErrNotFound } func (s *ClusterService) Validate(id uint) { cluster, err := s.Get(id) if err != nil { return } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() info, err := GetClusterInfo(ctx, cluster) if err != nil { s.Update(id, map[string]interface{}{"status": "error"}) return } s.Update(id, map[string]interface{}{ "status": "active", "version": info.Version, "nodesCount": len(info.Nodes), "lastConnected": time.Now(), "context": info.CurrentCtx, }) }