feat: complete E2E deployment flow with storage layered config and values template versioning

- 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
This commit is contained in:
Ivan087
2026-04-30 16:31:00 +08:00
parent 985369d40f
commit 47849042a7
42 changed files with 2029 additions and 255 deletions

View File

@ -3,6 +3,8 @@ package service
import (
"context"
"errors"
"fmt"
"strings"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
@ -13,6 +15,13 @@ var (
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
@ -33,14 +42,20 @@ func (s *StorageService) Create(
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
}
storage := entity.NewStorageBackend(workspaceID, ownerID, name, storageType, config)
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
@ -113,4 +128,81 @@ func (s *StorageService) Delete(ctx context.Context, id string) error {
// 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
}