- Instance deployment: charts browser, deploy modal, instances list - Values Template version management (create/history/rollback) - Storage layered config (cluster > workspace > shared priority) - Cluster credential decryptIfNeeded for mixed encrypted/plaintext kubeconfig - YAML syntax validation (client-side + server-side warning) - Frontend: charts, instances, storage, templates, admin pages - Backend: storage service, instance service, cluster service, helm client - Multi-Tenant Kubeconfig.md: added by user
187 lines
5.0 KiB
Go
187 lines
5.0 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/ocdp/cluster-service/internal/domain/entity"
|
|
"github.com/ocdp/cluster-service/internal/domain/repository"
|
|
"k8s.io/client-go/kubernetes"
|
|
"k8s.io/client-go/rest"
|
|
"k8s.io/client-go/tools/clientcmd"
|
|
)
|
|
|
|
// ClusterService 集群管理领域服务
|
|
type ClusterService struct {
|
|
clusterRepo repository.ClusterRepository
|
|
}
|
|
|
|
// NewClusterService 创建集群服务
|
|
func NewClusterService(clusterRepo repository.ClusterRepository) *ClusterService {
|
|
return &ClusterService{
|
|
clusterRepo: clusterRepo,
|
|
}
|
|
}
|
|
|
|
// CreateCluster 创建新集群
|
|
func (s *ClusterService) CreateCluster(ctx context.Context, cluster *entity.Cluster) error {
|
|
// 生成 ID
|
|
cluster.ID = uuid.New().String()
|
|
|
|
// 验证
|
|
if err := cluster.Validate(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 检查是否已存在
|
|
existingCluster, _ := s.clusterRepo.GetByName(ctx, cluster.Name)
|
|
if existingCluster != nil {
|
|
return entity.ErrClusterExists
|
|
}
|
|
|
|
return s.clusterRepo.Create(ctx, cluster)
|
|
}
|
|
|
|
// GetCluster 获取集群
|
|
func (s *ClusterService) GetCluster(ctx context.Context, id string) (*entity.Cluster, error) {
|
|
return s.clusterRepo.GetByID(ctx, id)
|
|
}
|
|
|
|
// UpdateCluster 更新集群
|
|
func (s *ClusterService) UpdateCluster(ctx context.Context, cluster *entity.Cluster) error {
|
|
// 检查是否存在
|
|
_, err := s.clusterRepo.GetByID(ctx, cluster.ID)
|
|
if err != nil {
|
|
return entity.ErrClusterNotFound
|
|
}
|
|
|
|
// 验证
|
|
if err := cluster.Validate(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return s.clusterRepo.Update(ctx, cluster)
|
|
}
|
|
|
|
// DeleteCluster 删除集群
|
|
func (s *ClusterService) DeleteCluster(ctx context.Context, id string) error {
|
|
// 检查是否存在
|
|
_, err := s.clusterRepo.GetByID(ctx, id)
|
|
if err != nil {
|
|
return entity.ErrClusterNotFound
|
|
}
|
|
|
|
return s.clusterRepo.Delete(ctx, id)
|
|
}
|
|
|
|
// ListClusters 列出所有集群
|
|
func (s *ClusterService) ListClusters(ctx context.Context) ([]*entity.Cluster, error) {
|
|
return s.clusterRepo.List(ctx)
|
|
}
|
|
|
|
// ListByWorkspace 列出指定 workspace 的集群(包括共享集群)
|
|
func (s *ClusterService) ListByWorkspace(ctx context.Context, workspaceID string) ([]*entity.Cluster, error) {
|
|
return s.clusterRepo.GetByWorkspace(ctx, workspaceID)
|
|
}
|
|
|
|
// GetSharedClusters 获取所有共享集群
|
|
func (s *ClusterService) GetSharedClusters(ctx context.Context) ([]*entity.Cluster, error) {
|
|
return s.clusterRepo.GetShared(ctx)
|
|
}
|
|
|
|
// TestConnection 测试集群连接是否可用
|
|
func (s *ClusterService) TestConnection(ctx context.Context, cluster *entity.Cluster) error {
|
|
// Mock 模式直接返回成功
|
|
if os.Getenv("ADAPTER_MODE") == "mock" {
|
|
return nil
|
|
}
|
|
|
|
// 尝试创建 k8s client
|
|
config, err := s.createRestConfig(cluster)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create k8s config: %w", err)
|
|
}
|
|
|
|
// 设置超时
|
|
config.Timeout = 30 * 1000000000 // 30秒 (nanoseconds)
|
|
|
|
clientset, err := kubernetes.NewForConfig(config)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create k8s client: %w", err)
|
|
}
|
|
|
|
// 测试连接 - 获取 version 信息
|
|
version, err := clientset.ServerVersion()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to connect to cluster: %w", err)
|
|
}
|
|
|
|
if version == nil {
|
|
return fmt.Errorf("cluster returned nil version")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// createRestConfig 从 cluster 实体创建 k8s REST 配置
|
|
func (s *ClusterService) createRestConfig(cluster *entity.Cluster) (*rest.Config, error) {
|
|
// 优先使用 kubeconfig 格式(如果 CAData 包含完整的 kubeconfig 内容)
|
|
if len(cluster.CAData) > 100 && (cluster.CAData[:11] == "apiVersion:" || cluster.CAData[:5] == "kind:") {
|
|
return clientcmd.RESTConfigFromKubeConfig([]byte(cluster.CAData))
|
|
}
|
|
|
|
// 使用证书或 token 认证
|
|
config := &rest.Config{
|
|
Host: cluster.Host,
|
|
}
|
|
|
|
if cluster.CertData != "" && cluster.KeyData != "" {
|
|
// 尝试解码 base64 编码的证书,如果失败则尝试原始 PEM
|
|
var caData, certData, keyData []byte
|
|
var decodeErr error
|
|
|
|
// 先尝试 base64 解码
|
|
caData, decodeErr = base64.StdEncoding.DecodeString(cluster.CAData)
|
|
if decodeErr != nil {
|
|
// base64 解码失败,可能是原始 PEM
|
|
caData = []byte(cluster.CAData)
|
|
}
|
|
|
|
certData, decodeErr = base64.StdEncoding.DecodeString(cluster.CertData)
|
|
if decodeErr != nil {
|
|
certData = []byte(cluster.CertData)
|
|
}
|
|
|
|
keyData, decodeErr = base64.StdEncoding.DecodeString(cluster.KeyData)
|
|
if decodeErr != nil {
|
|
keyData = []byte(cluster.KeyData)
|
|
}
|
|
|
|
config.TLSClientConfig = rest.TLSClientConfig{
|
|
CAData: caData,
|
|
CertData: certData,
|
|
KeyData: keyData,
|
|
Insecure: false,
|
|
}
|
|
} else if cluster.Token != "" {
|
|
config.BearerToken = cluster.Token
|
|
} else {
|
|
// 尝试使用本地 kubeconfig
|
|
kubeconfig := os.Getenv("KUBECONFIG")
|
|
if kubeconfig == "" {
|
|
kubeconfig = ".kube/config"
|
|
}
|
|
// 尝试从文件加载 kubeconfig
|
|
if _, err := os.Stat(kubeconfig); err != nil {
|
|
return nil, fmt.Errorf("no valid credentials found for cluster %s (no cert/key/token, and kubeconfig file not found: %s)", cluster.Name, kubeconfig)
|
|
}
|
|
return clientcmd.BuildConfigFromFlags("", kubeconfig)
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|