- 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
208 lines
6.2 KiB
Go
208 lines
6.2 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||
"github.com/ocdp/cluster-service/internal/domain/repository"
|
||
)
|
||
|
||
var (
|
||
ErrStorageNotFound = errors.New("storage backend not found")
|
||
ErrStorageExists = errors.New("storage backend already exists")
|
||
)
|
||
|
||
// StorageResolution 存储分层解析结果
|
||
type StorageResolution struct {
|
||
Storage *entity.StorageBackend // 最终选中的 storage
|
||
ValuesYAML string // 转换为 YAML 的 values
|
||
Source string // 来源: "workspace", "cluster", "shared"
|
||
}
|
||
|
||
// StorageService 存储后端领域服务
|
||
type StorageService struct {
|
||
storageRepo repository.StorageRepository
|
||
}
|
||
|
||
// NewStorageService 创建存储后端服务
|
||
func NewStorageService(storageRepo repository.StorageRepository) *StorageService {
|
||
return &StorageService{
|
||
storageRepo: storageRepo,
|
||
}
|
||
}
|
||
|
||
// Create 创建存储后端
|
||
func (s *StorageService) Create(
|
||
ctx context.Context,
|
||
workspaceID, ownerID, name string,
|
||
storageType entity.StorageType,
|
||
config entity.StorageConfig,
|
||
description string,
|
||
isDefault, isShared bool,
|
||
clusterID string,
|
||
) (*entity.StorageBackend, error) {
|
||
// 检查名称是否已存在(同一 workspace 或同一 cluster 下不能重复)
|
||
existing, _ := s.storageRepo.GetByName(ctx, workspaceID, name)
|
||
if existing != nil {
|
||
return nil, ErrStorageExists
|
||
}
|
||
|
||
var storage *entity.StorageBackend
|
||
if clusterID != "" {
|
||
storage = entity.NewClusterStorageBackend(workspaceID, clusterID, ownerID, name, storageType, config)
|
||
} else {
|
||
storage = entity.NewStorageBackend(workspaceID, ownerID, name, storageType, config)
|
||
}
|
||
storage.Description = description
|
||
storage.IsDefault = isDefault
|
||
storage.IsShared = isShared
|
||
|
||
if err := s.storageRepo.Create(ctx, storage); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return storage, nil
|
||
}
|
||
|
||
// GetByID 获取存储后端
|
||
func (s *StorageService) GetByID(ctx context.Context, id string) (*entity.StorageBackend, error) {
|
||
storage, err := s.storageRepo.GetByID(ctx, id)
|
||
if err != nil {
|
||
return nil, ErrStorageNotFound
|
||
}
|
||
return storage, nil
|
||
}
|
||
|
||
// GetByWorkspace 获取工作空间的所有存储后端
|
||
func (s *StorageService) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.StorageBackend, error) {
|
||
return s.storageRepo.GetByWorkspace(ctx, workspaceID)
|
||
}
|
||
|
||
// GetShared 获取所有共享存储后端
|
||
func (s *StorageService) GetShared(ctx context.Context) ([]*entity.StorageBackend, error) {
|
||
return s.storageRepo.GetShared(ctx)
|
||
}
|
||
|
||
// GetDefault 获取工作空间的默认存储后端
|
||
func (s *StorageService) GetDefault(ctx context.Context, workspaceID string) (*entity.StorageBackend, error) {
|
||
return s.storageRepo.GetDefault(ctx, workspaceID)
|
||
}
|
||
|
||
// Update 更新存储后端
|
||
func (s *StorageService) Update(
|
||
ctx context.Context,
|
||
id, name, description string,
|
||
storageType entity.StorageType,
|
||
config entity.StorageConfig,
|
||
isDefault, isShared bool,
|
||
) (*entity.StorageBackend, error) {
|
||
storage, err := s.storageRepo.GetByID(ctx, id)
|
||
if err != nil {
|
||
return nil, ErrStorageNotFound
|
||
}
|
||
|
||
if name != "" {
|
||
storage.Name = name
|
||
}
|
||
storage.Description = description
|
||
storage.Type = storageType
|
||
storage.Config = config
|
||
storage.IsDefault = isDefault
|
||
storage.IsShared = isShared
|
||
|
||
if err := s.storageRepo.Update(ctx, storage); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return storage, nil
|
||
}
|
||
|
||
// Delete 删除存储后端
|
||
func (s *StorageService) Delete(ctx context.Context, id string) error {
|
||
return s.storageRepo.Delete(ctx, id)
|
||
}
|
||
|
||
// List 列出所有存储后端(管理员用)
|
||
func (s *StorageService) List(ctx context.Context) ([]*entity.StorageBackend, error) {
|
||
return s.storageRepo.List(ctx)
|
||
}
|
||
|
||
// ResolveStorageConfig 分层解析存储配置
|
||
// 优先级:User Override > Workspace Default > Cluster Default > Shared Default
|
||
func (s *StorageService) ResolveStorageConfig(
|
||
ctx context.Context,
|
||
clusterID, workspaceID string,
|
||
) (*StorageResolution, error) {
|
||
// 1. 查找 workspace-level 默认存储
|
||
if workspaceID != "" {
|
||
wsStorage, err := s.storageRepo.GetDefault(ctx, workspaceID)
|
||
if err == nil && wsStorage != nil {
|
||
yaml, _ := storageToValuesYAML(wsStorage)
|
||
return &StorageResolution{
|
||
Storage: wsStorage,
|
||
ValuesYAML: yaml,
|
||
Source: "workspace",
|
||
}, nil
|
||
}
|
||
}
|
||
|
||
// 2. 查找 cluster-level 默认存储
|
||
if clusterID != "" {
|
||
clusterStorage, err := s.storageRepo.GetDefaultByCluster(ctx, clusterID)
|
||
if err == nil && clusterStorage != nil {
|
||
yaml, _ := storageToValuesYAML(clusterStorage)
|
||
return &StorageResolution{
|
||
Storage: clusterStorage,
|
||
ValuesYAML: yaml,
|
||
Source: "cluster",
|
||
}, nil
|
||
}
|
||
}
|
||
|
||
// 3. 查找 shared 默认存储
|
||
sharedStorages, err := s.storageRepo.GetShared(ctx)
|
||
if err == nil {
|
||
for _, s := range sharedStorages {
|
||
if s.IsDefault {
|
||
yaml, _ := storageToValuesYAML(s)
|
||
return &StorageResolution{
|
||
Storage: s,
|
||
ValuesYAML: yaml,
|
||
Source: "shared",
|
||
}, nil
|
||
}
|
||
}
|
||
}
|
||
|
||
return nil, nil
|
||
}
|
||
|
||
// storageToValuesYAML 将 storage config 转换为 values.yaml 格式
|
||
func storageToValuesYAML(storage *entity.StorageBackend) (string, error) {
|
||
if storage == nil {
|
||
return "", nil
|
||
}
|
||
|
||
switch storage.Type {
|
||
case entity.StorageTypeNFS:
|
||
if storage.Config.NFS != nil {
|
||
// Format as nfs-server/path storageClass so Helm charts like bitnami/nginx
|
||
// can use: persistence.storageClass: "nfs-server/path"
|
||
nfsSC := fmt.Sprintf("nfs-%s-%s", storage.Config.NFS.Server, strings.TrimPrefix(storage.Config.NFS.Path, "/"))
|
||
return fmt.Sprintf("persistence:\n enabled: true\n storageClass: \"%s\"\n existingClaim: \"\"\n mountOptions:\n - hard\n - nfsvers=4.1\n dataSource: {}\n# NFS Server: %s\n# NFS Path: %s", nfsSC, storage.Config.NFS.Server, storage.Config.NFS.Path), nil
|
||
}
|
||
case entity.StorageTypePV:
|
||
if storage.Config.PV != nil {
|
||
return fmt.Sprintf("persistence:\n enabled: true\n storageClass: \"%s\"\n size: %s\n accessModes: %v\n existingClaim: \"\"", storage.Config.PV.StorageClassName, storage.Config.PV.Capacity, storage.Config.PV.AccessModes), nil
|
||
}
|
||
case entity.StorageTypeHostPath:
|
||
if storage.Config.HostPath != nil {
|
||
return fmt.Sprintf("persistence:\n enabled: true\n hostPath: \"%s\"\n existingClaim: \"\"", storage.Config.HostPath.Path), nil
|
||
}
|
||
}
|
||
|
||
return "", nil
|
||
} |