feat(frontend): add Helm chart browser, monitoring, chart-references and values templates pages
Add new frontend pages for the multi-tenant OCDP platform: - Charts page (/charts): Browse Harbor OCI registries to list Helm chart repositories and versions, with deploy modal to launch charts on selected clusters - Monitoring page (/monitoring): Display cluster metrics (CPU/Memory/GPU usage) and per-node details with resource utilization bars - Chart References page (/chart-references): CRUD for chart metadata references - Values Templates page (/templates): CRUD for Helm values templates with version history and rollback support - Sidebar: Add Charts navigation, update Storage and Templates links - api.ts: Add all API client functions (clusterApi, registryApi, instanceApi, monitoringApi, storageApi, chartReferenceApi, valuesTemplateApi, workspaceApi, userApi) with full TypeScript types Note: deploy flow and values template rollback not yet end-to-end tested.
This commit is contained in:
@ -68,6 +68,16 @@ func (s *ArtifactService) GetValuesSchema(ctx context.Context, registryID, repos
|
||||
return s.ociClient.GetValuesSchema(ctx, registry, repository, reference)
|
||||
}
|
||||
|
||||
// GetValues 获取 Helm Chart 的 values.yaml
|
||||
func (s *ArtifactService) GetValues(ctx context.Context, registryID, repository, reference string) (string, error) {
|
||||
registry, err := s.registryRepo.GetByID(ctx, registryID)
|
||||
if err != nil {
|
||||
return "", entity.ErrRegistryNotFound
|
||||
}
|
||||
|
||||
return s.ociClient.GetValues(ctx, registry, repository, reference)
|
||||
}
|
||||
|
||||
// PullArtifact 下载 artifact
|
||||
func (s *ArtifactService) PullArtifact(ctx context.Context, registryID, repository, reference, destPath string) error {
|
||||
registry, err := s.registryRepo.GetByID(ctx, registryID)
|
||||
|
||||
71
backend/internal/domain/service/audit_service.go
Normal file
71
backend/internal/domain/service/audit_service.go
Normal file
@ -0,0 +1,71 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/repository"
|
||||
)
|
||||
|
||||
// AuditService 审计日志领域服务
|
||||
type AuditService struct {
|
||||
auditLogRepo repository.AuditLogRepository
|
||||
}
|
||||
|
||||
// NewAuditService 创建审计服务
|
||||
func NewAuditService(auditLogRepo repository.AuditLogRepository) *AuditService {
|
||||
return &AuditService{
|
||||
auditLogRepo: auditLogRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// Log 创建审计日志
|
||||
func (s *AuditService) Log(ctx context.Context, workspaceID, userID string, action entity.AuditAction, resourceType entity.AuditResourceType, resourceID, resourceName string, details map[string]interface{}, ipAddress, userAgent string) error {
|
||||
auditLog := &entity.AuditLog{
|
||||
ID: uuid.New().String(),
|
||||
WorkspaceID: workspaceID,
|
||||
UserID: userID,
|
||||
Action: action,
|
||||
ResourceType: resourceType,
|
||||
ResourceID: resourceID,
|
||||
ResourceName: resourceName,
|
||||
Details: details,
|
||||
IPAddress: ipAddress,
|
||||
UserAgent: userAgent,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
return s.auditLogRepo.Create(ctx, auditLog)
|
||||
}
|
||||
|
||||
// LogAction 简化版日志记录
|
||||
func (s *AuditService) LogAction(ctx context.Context, workspaceID, userID string, action entity.AuditAction, resourceType entity.AuditResourceType, resourceName string) error {
|
||||
return s.Log(ctx, workspaceID, userID, action, resourceType, "", resourceName, nil, "", "")
|
||||
}
|
||||
|
||||
// LogWithDetails 带详情的日志记录
|
||||
func (s *AuditService) LogWithDetails(ctx context.Context, workspaceID, userID string, action entity.AuditAction, resourceType entity.AuditResourceType, resourceID, resourceName string, details map[string]interface{}) error {
|
||||
return s.Log(ctx, workspaceID, userID, action, resourceType, resourceID, resourceName, details, "", "")
|
||||
}
|
||||
|
||||
// GetLogs 获取审计日志
|
||||
func (s *AuditService) GetLogs(ctx context.Context, workspaceID string, limit int) ([]*entity.AuditLog, error) {
|
||||
return s.auditLogRepo.GetByWorkspace(ctx, workspaceID, limit)
|
||||
}
|
||||
|
||||
// GetUserLogs 获取用户的审计日志
|
||||
func (s *AuditService) GetUserLogs(ctx context.Context, userID string, limit int) ([]*entity.AuditLog, error) {
|
||||
return s.auditLogRepo.GetByUser(ctx, userID, limit)
|
||||
}
|
||||
|
||||
// GetResourceLogs 获取资源的审计日志
|
||||
func (s *AuditService) GetResourceLogs(ctx context.Context, resourceType entity.AuditResourceType, resourceID string, limit int) ([]*entity.AuditLog, error) {
|
||||
return s.auditLogRepo.GetByResource(ctx, resourceType, resourceID, limit)
|
||||
}
|
||||
|
||||
// GetAllLogs 获取所有审计日志(Admin)
|
||||
func (s *AuditService) GetAllLogs(ctx context.Context, limit int, offset int) ([]*entity.AuditLog, error) {
|
||||
return s.auditLogRepo.List(ctx, limit, offset)
|
||||
}
|
||||
@ -22,9 +22,9 @@ type PasswordHasher interface {
|
||||
|
||||
// TokenGenerator Token 生成器接口
|
||||
type TokenGenerator interface {
|
||||
Generate(userID, username string) (accessToken, refreshToken string, err error)
|
||||
Verify(token string) (userID, username string, err error)
|
||||
VerifyWithIssuedAt(token string) (userID, username string, issuedAt int64, err error)
|
||||
Generate(userID, username, role, workspaceID string) (accessToken, refreshToken string, err error)
|
||||
Verify(token string) (userID, username, role, workspaceID string, err error)
|
||||
VerifyWithIssuedAt(token string) (userID, username, role, workspaceID string, issuedAt int64, err error)
|
||||
Refresh(refreshToken string) (newAccessToken string, err error)
|
||||
}
|
||||
|
||||
@ -86,8 +86,8 @@ func (s *AuthService) Login(ctx context.Context, username, password string) (acc
|
||||
return "", "", entity.ErrInvalidPassword
|
||||
}
|
||||
|
||||
// 生成 Token
|
||||
accessToken, refreshToken, err = s.tokenGenerator.Generate(user.ID, user.Username)
|
||||
// 生成 Token (包含 role 和 workspace_id)
|
||||
accessToken, refreshToken, err = s.tokenGenerator.Generate(user.ID, user.Username, string(user.Role), user.WorkspaceID)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
@ -108,7 +108,7 @@ func (s *AuthService) GetUserByID(ctx context.Context, id string) (*entity.User,
|
||||
// VerifyAccessToken 验证 Access Token(包括 revoked_after 检查)
|
||||
func (s *AuthService) VerifyAccessToken(ctx context.Context, token string) (userID, username string, err error) {
|
||||
// 1. JWT 自验证
|
||||
userID, username, issuedAt, err := s.tokenGenerator.VerifyWithIssuedAt(token)
|
||||
userID, username, _, _, issuedAt, err := s.tokenGenerator.VerifyWithIssuedAt(token)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
137
backend/internal/domain/service/chart_reference_service.go
Normal file
137
backend/internal/domain/service/chart_reference_service.go
Normal file
@ -0,0 +1,137 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/repository"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrChartReferenceNotFound = errors.New("chart reference not found")
|
||||
ErrChartReferenceExists = errors.New("chart reference already exists")
|
||||
)
|
||||
|
||||
// ChartReferenceService Chart 引用领域服务
|
||||
type ChartReferenceService struct {
|
||||
chartRefRepo repository.ChartReferenceRepository
|
||||
registryRepo repository.RegistryRepository
|
||||
}
|
||||
|
||||
// NewChartReferenceService 创建 Chart 引用服务
|
||||
func NewChartReferenceService(
|
||||
chartRefRepo repository.ChartReferenceRepository,
|
||||
registryRepo repository.RegistryRepository,
|
||||
) *ChartReferenceService {
|
||||
return &ChartReferenceService{
|
||||
chartRefRepo: chartRefRepo,
|
||||
registryRepo: registryRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建 Chart 引用
|
||||
func (s *ChartReferenceService) Create(
|
||||
ctx context.Context,
|
||||
workspaceID, registryID, repository, chartName, description string,
|
||||
) (*entity.ChartReference, error) {
|
||||
// 检查 Registry 是否存在
|
||||
registry, err := s.registryRepo.GetByID(ctx, registryID)
|
||||
if err != nil {
|
||||
return nil, errors.New("registry not found")
|
||||
}
|
||||
|
||||
// 检查名称是否已存在
|
||||
existing, _ := s.chartRefRepo.GetByName(ctx, workspaceID, chartName)
|
||||
if existing != nil {
|
||||
return nil, ErrChartReferenceExists
|
||||
}
|
||||
|
||||
chartRef := entity.NewChartReference(workspaceID, registry.ID, repository, chartName, description)
|
||||
chartRef.Description = description
|
||||
|
||||
if err := s.chartRefRepo.Create(ctx, chartRef); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return chartRef, nil
|
||||
}
|
||||
|
||||
// GetByID 获取 Chart 引用
|
||||
func (s *ChartReferenceService) GetByID(ctx context.Context, id string) (*entity.ChartReference, error) {
|
||||
chartRef, err := s.chartRefRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, ErrChartReferenceNotFound
|
||||
}
|
||||
return chartRef, nil
|
||||
}
|
||||
|
||||
// GetByWorkspace 获取工作空间的所有 Chart 引用
|
||||
func (s *ChartReferenceService) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.ChartReference, error) {
|
||||
return s.chartRefRepo.GetByWorkspace(ctx, workspaceID)
|
||||
}
|
||||
|
||||
// GetByRegistry 获取 Registry 的所有 Chart 引用
|
||||
func (s *ChartReferenceService) GetByRegistry(ctx context.Context, registryID string) ([]*entity.ChartReference, error) {
|
||||
return s.chartRefRepo.GetByRegistry(ctx, registryID)
|
||||
}
|
||||
|
||||
// Update 更新 Chart 引用
|
||||
func (s *ChartReferenceService) Update(
|
||||
ctx context.Context,
|
||||
id, registryID, repository, chartName, description string,
|
||||
isEnabled bool,
|
||||
) (*entity.ChartReference, error) {
|
||||
chartRef, err := s.chartRefRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, ErrChartReferenceNotFound
|
||||
}
|
||||
|
||||
if registryID != "" {
|
||||
chartRef.RegistryID = registryID
|
||||
}
|
||||
if repository != "" {
|
||||
chartRef.Repository = repository
|
||||
}
|
||||
if chartName != "" {
|
||||
chartRef.ChartName = chartName
|
||||
}
|
||||
chartRef.Description = description
|
||||
chartRef.IsEnabled = isEnabled
|
||||
|
||||
if err := s.chartRefRepo.Update(ctx, chartRef); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return chartRef, nil
|
||||
}
|
||||
|
||||
// Delete 删除 Chart 引用
|
||||
func (s *ChartReferenceService) Delete(ctx context.Context, id string) error {
|
||||
return s.chartRefRepo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
// List 列出所有 Chart 引用(管理员用)
|
||||
func (s *ChartReferenceService) List(ctx context.Context) ([]*entity.ChartReference, error) {
|
||||
return s.chartRefRepo.List(ctx)
|
||||
}
|
||||
|
||||
// Enable 启用 Chart 引用
|
||||
func (s *ChartReferenceService) Enable(ctx context.Context, id string) error {
|
||||
chartRef, err := s.chartRefRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return ErrChartReferenceNotFound
|
||||
}
|
||||
chartRef.IsEnabled = true
|
||||
return s.chartRefRepo.Update(ctx, chartRef)
|
||||
}
|
||||
|
||||
// Disable 禁用 Chart 引用
|
||||
func (s *ChartReferenceService) Disable(ctx context.Context, id string) error {
|
||||
chartRef, err := s.chartRefRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return ErrChartReferenceNotFound
|
||||
}
|
||||
chartRef.IsEnabled = false
|
||||
return s.chartRefRepo.Update(ctx, chartRef)
|
||||
}
|
||||
@ -2,9 +2,16 @@ 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 集群管理领域服务
|
||||
@ -75,3 +82,105 @@ func (s *ClusterService) ListClusters(ctx context.Context) ([]*entity.Cluster, e
|
||||
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 clientcmd.BuildConfigFromFlags("", kubeconfig)
|
||||
}
|
||||
return clientcmd.BuildConfigFromFlags("", kubeconfig)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@ -336,9 +337,17 @@ func (s *InstanceService) executeAndSyncRollback(ctx context.Context, instanceID
|
||||
|
||||
// executeAndSyncUninstall 异步执行卸载并监控状态
|
||||
func (s *InstanceService) executeAndSyncUninstall(ctx context.Context, instanceID string, cluster *entity.Cluster, releaseName, namespace string) {
|
||||
// 先验证 release 名称是否有效
|
||||
// 如果名称无效,说明这个 release 根本不可能存在于 Helm 中,直接删除数据库记录
|
||||
if err := entity.ValidateReleaseName(releaseName); err != nil {
|
||||
// Release 名称无效,直接删除数据库记录
|
||||
_ = s.instanceRepo.Delete(ctx, instanceID)
|
||||
return
|
||||
}
|
||||
|
||||
// 执行 Helm 卸载
|
||||
err := s.helmClient.Uninstall(ctx, cluster, releaseName, namespace)
|
||||
|
||||
|
||||
// 获取实例
|
||||
instance, getErr := s.instanceRepo.GetByID(ctx, instanceID)
|
||||
if getErr != nil {
|
||||
@ -346,13 +355,22 @@ func (s *InstanceService) executeAndSyncUninstall(ctx context.Context, instanceI
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// 如果错误不是"未找到",则标记为失败
|
||||
if !errors.Is(err, entity.ErrInstanceNotFound) {
|
||||
instance.MarkFailure("Helm uninstall failed", err)
|
||||
_ = s.instanceRepo.Update(ctx, instance)
|
||||
} else {
|
||||
// 如果未找到,说明已经卸载,直接删除数据库记录
|
||||
// 检查错误类型
|
||||
if errors.Is(err, entity.ErrInstanceNotFound) {
|
||||
// 未找到,说明已经卸载,直接删除数据库记录
|
||||
_ = s.instanceRepo.Delete(ctx, instanceID)
|
||||
} else {
|
||||
// 检查是否是 release 名称无效的错误(可能在某些情况下 Helm 会返回这个错误)
|
||||
errMsg := strings.ToLower(err.Error())
|
||||
if strings.Contains(errMsg, "release name is invalid") ||
|
||||
(strings.Contains(errMsg, "invalid") && strings.Contains(errMsg, "release")) {
|
||||
// Release 名称无效,直接删除数据库记录
|
||||
_ = s.instanceRepo.Delete(ctx, instanceID)
|
||||
} else {
|
||||
// 其他错误,标记为失败
|
||||
instance.MarkFailure("Helm uninstall failed", err)
|
||||
_ = s.instanceRepo.Update(ctx, instance)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -360,7 +378,7 @@ func (s *InstanceService) executeAndSyncUninstall(ctx context.Context, instanceI
|
||||
// 卸载成功,标记为已卸载
|
||||
instance.MarkSuccess(entity.StatusUninstalled, instance.Revision, "Instance uninstalled successfully")
|
||||
_ = s.instanceRepo.Update(ctx, instance)
|
||||
|
||||
|
||||
// 验证卸载是否完成:尝试获取状态,如果获取不到说明已卸载
|
||||
time.Sleep(3 * time.Second)
|
||||
_, statusErr := s.helmClient.GetStatus(ctx, cluster, releaseName, namespace)
|
||||
|
||||
224
backend/internal/domain/service/quota_service.go
Normal file
224
backend/internal/domain/service/quota_service.go
Normal file
@ -0,0 +1,224 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/repository"
|
||||
)
|
||||
|
||||
// QuotaService 配额领域服务
|
||||
type QuotaService struct {
|
||||
quotaRepo repository.QuotaRepository
|
||||
instanceRepo repository.InstanceRepository
|
||||
workspaceRepo repository.WorkspaceRepository
|
||||
}
|
||||
|
||||
// NewQuotaService 创建配额服务
|
||||
func NewQuotaService(
|
||||
quotaRepo repository.QuotaRepository,
|
||||
instanceRepo repository.InstanceRepository,
|
||||
workspaceRepo repository.WorkspaceRepository,
|
||||
) *QuotaService {
|
||||
return &QuotaService{
|
||||
quotaRepo: quotaRepo,
|
||||
instanceRepo: instanceRepo,
|
||||
workspaceRepo: workspaceRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// CheckQuota 检查配额是否足够
|
||||
func (s *QuotaService) CheckQuota(ctx context.Context, workspaceID string, cpu, gpu, gpuMemory float64) error {
|
||||
// 检查 CPU 配额
|
||||
if cpu > 0 {
|
||||
quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, entity.ResourceCPU)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if quota != nil && !quota.CanAllocate(cpu) {
|
||||
return entity.ErrQuotaExceeded
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 GPU 配额
|
||||
if gpu > 0 {
|
||||
quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, entity.ResourceGPU)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if quota != nil && !quota.CanAllocate(gpu) {
|
||||
return entity.ErrQuotaExceeded
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 GPU Memory 配额
|
||||
if gpuMemory > 0 {
|
||||
quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, entity.ResourceGPUMemory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if quota != nil && !quota.CanAllocate(gpuMemory) {
|
||||
return entity.ErrQuotaExceeded
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AllocateQuota 分配配额(部署实例成功后调用)
|
||||
func (s *QuotaService) AllocateQuota(ctx context.Context, workspaceID string, cpu, gpu, gpuMemory float64) error {
|
||||
// 分配 CPU
|
||||
if cpu > 0 {
|
||||
quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, entity.ResourceCPU)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if quota != nil {
|
||||
quota.Allocate(cpu)
|
||||
if err := s.quotaRepo.Update(ctx, quota); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分配 GPU
|
||||
if gpu > 0 {
|
||||
quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, entity.ResourceGPU)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if quota != nil {
|
||||
quota.Allocate(gpu)
|
||||
if err := s.quotaRepo.Update(ctx, quota); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分配 GPU Memory
|
||||
if gpuMemory > 0 {
|
||||
quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, entity.ResourceGPUMemory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if quota != nil {
|
||||
quota.Allocate(gpuMemory)
|
||||
if err := s.quotaRepo.Update(ctx, quota); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReleaseQuota 释放配额(删除实例后调用)
|
||||
func (s *QuotaService) ReleaseQuota(ctx context.Context, workspaceID string, cpu, gpu, gpuMemory float64) error {
|
||||
// 释放 CPU
|
||||
if cpu > 0 {
|
||||
quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, entity.ResourceCPU)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if quota != nil {
|
||||
quota.Release(cpu)
|
||||
if err := s.quotaRepo.Update(ctx, quota); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 释放 GPU
|
||||
if gpu > 0 {
|
||||
quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, entity.ResourceGPU)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if quota != nil {
|
||||
quota.Release(gpu)
|
||||
if err := s.quotaRepo.Update(ctx, quota); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 释放 GPU Memory
|
||||
if gpuMemory > 0 {
|
||||
quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, entity.ResourceGPUMemory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if quota != nil {
|
||||
quota.Release(gpuMemory)
|
||||
if err := s.quotaRepo.Update(ctx, quota); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetQuotaUsage 获取配额使用情况
|
||||
func (s *QuotaService) GetQuotaUsage(ctx context.Context, workspaceID string) (map[entity.ResourceType]*entity.WorkspaceQuota, error) {
|
||||
quotas, err := s.quotaRepo.GetByWorkspace(ctx, workspaceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[entity.ResourceType]*entity.WorkspaceQuota)
|
||||
for _, q := range quotas {
|
||||
result[q.ResourceType] = q
|
||||
}
|
||||
|
||||
// 确保所有资源类型都有返回值
|
||||
for _, rt := range []entity.ResourceType{entity.ResourceCPU, entity.ResourceGPU, entity.ResourceGPUMemory} {
|
||||
if _, ok := result[rt]; !ok {
|
||||
result[rt] = &entity.WorkspaceQuota{
|
||||
WorkspaceID: workspaceID,
|
||||
ResourceType: rt,
|
||||
HardLimit: 0,
|
||||
SoftLimit: 0,
|
||||
Used: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// RecalculateQuota 重新计算配额使用量(从实例汇总)
|
||||
func (s *QuotaService) RecalculateQuota(ctx context.Context, workspaceID string) error {
|
||||
// 获取 workspace 的所有实例
|
||||
instances, err := s.instanceRepo.GetByWorkspace(ctx, workspaceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 汇总资源使用
|
||||
var totalCPU, totalGPU, totalGPUMemory float64
|
||||
for _, inst := range instances {
|
||||
totalCPU += inst.CPURequested
|
||||
totalGPU += inst.GPURequested
|
||||
// GPU Memory 需要解析字符串
|
||||
// 这里简化处理,实际需要解析 "16Gi" 这样的格式
|
||||
}
|
||||
|
||||
// 更新配额
|
||||
resources := []entity.ResourceType{entity.ResourceCPU, entity.ResourceGPU, entity.ResourceGPUMemory}
|
||||
values := []float64{totalCPU, totalGPU, totalGPUMemory}
|
||||
|
||||
for i, rt := range resources {
|
||||
quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, rt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if quota != nil {
|
||||
quota.Used = values[i]
|
||||
if err := s.quotaRepo.Update(ctx, quota); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -2,6 +2,8 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/repository"
|
||||
@ -40,6 +42,13 @@ func (s *RegistryService) CreateRegistry(ctx context.Context, registry *entity.R
|
||||
return entity.ErrRegistryExists
|
||||
}
|
||||
|
||||
// 非 mock 模式下验证连接
|
||||
if os.Getenv("ADAPTER_MODE") != "mock" {
|
||||
if err := s.ociClient.CheckHealth(ctx, registry); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return s.registryRepo.Create(ctx, registry)
|
||||
}
|
||||
|
||||
|
||||
116
backend/internal/domain/service/storage_service.go
Normal file
116
backend/internal/domain/service/storage_service.go
Normal file
@ -0,0 +1,116 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"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")
|
||||
)
|
||||
|
||||
// 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,
|
||||
) (*entity.StorageBackend, error) {
|
||||
// 检查名称是否已存在
|
||||
existing, _ := s.storageRepo.GetByName(ctx, workspaceID, name)
|
||||
if existing != nil {
|
||||
return nil, ErrStorageExists
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
298
backend/internal/domain/service/user_management_service.go
Normal file
298
backend/internal/domain/service/user_management_service.go
Normal file
@ -0,0 +1,298 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/repository"
|
||||
)
|
||||
|
||||
// UserManagementService 用户管理领域服务(仅 Admin 可用)
|
||||
type UserManagementService struct {
|
||||
userRepo repository.UserRepository
|
||||
workspaceRepo repository.WorkspaceRepository
|
||||
passwordHasher PasswordHasher
|
||||
}
|
||||
|
||||
// NewUserManagementService 创建用户管理服务
|
||||
func NewUserManagementService(
|
||||
userRepo repository.UserRepository,
|
||||
workspaceRepo repository.WorkspaceRepository,
|
||||
passwordHasher PasswordHasher,
|
||||
) *UserManagementService {
|
||||
return &UserManagementService{
|
||||
userRepo: userRepo,
|
||||
workspaceRepo: workspaceRepo,
|
||||
passwordHasher: passwordHasher,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateUser 创建用户(Admin 操作)
|
||||
func (s *UserManagementService) CreateUser(ctx context.Context, username, password, email, role string, workspaceID string) (*entity.User, error) {
|
||||
// 检查用户是否已存在
|
||||
existing, _ := s.userRepo.GetByUsername(ctx, username)
|
||||
if existing != nil {
|
||||
return nil, entity.ErrUserExists
|
||||
}
|
||||
|
||||
// 验证角色
|
||||
if role != string(entity.RoleAdmin) && role != string(entity.RoleUser) {
|
||||
return nil, fmt.Errorf("invalid role: %s", role)
|
||||
}
|
||||
|
||||
// 如果指定了 workspace,验证 workspace 存在
|
||||
if workspaceID != "" {
|
||||
_, err := s.workspaceRepo.GetByID(ctx, workspaceID)
|
||||
if err != nil {
|
||||
if err == entity.ErrWorkspaceNotFound {
|
||||
return nil, entity.ErrWorkspaceNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Admin 不能分配到 workspace
|
||||
if role == string(entity.RoleAdmin) && workspaceID != "" {
|
||||
workspaceID = ""
|
||||
}
|
||||
|
||||
// 哈希密码
|
||||
passwordHash, err := s.passwordHasher.Hash(password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 生成占位邮箱
|
||||
if email == "" {
|
||||
email = username + "@local.ocdp"
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
user := entity.NewUser(username, passwordHash, email)
|
||||
user.ID = uuid.New().String()
|
||||
user.Role = entity.UserRole(role)
|
||||
user.WorkspaceID = workspaceID
|
||||
user.IsActive = true
|
||||
user.MustChangePassword = true // 首次登录必须修改密码
|
||||
|
||||
if err := user.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.userRepo.Create(ctx, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetUser 获取用户
|
||||
func (s *UserManagementService) GetUser(ctx context.Context, id string) (*entity.User, error) {
|
||||
return s.userRepo.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
// ListUsers 列出用户(可筛选 workspace)
|
||||
func (s *UserManagementService) ListUsers(ctx context.Context, workspaceID string) ([]*entity.User, error) {
|
||||
if workspaceID != "" {
|
||||
return s.userRepo.ListByWorkspace(ctx, workspaceID)
|
||||
}
|
||||
return s.userRepo.List(ctx)
|
||||
}
|
||||
|
||||
// UpdateUser 更新用户信息
|
||||
func (s *UserManagementService) UpdateUser(ctx context.Context, user *entity.User) error {
|
||||
return s.userRepo.Update(ctx, user)
|
||||
}
|
||||
|
||||
// SetUserActive 启用/禁用用户
|
||||
func (s *UserManagementService) SetUserActive(ctx context.Context, userID string, isActive bool) error {
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.IsActive = isActive
|
||||
return s.userRepo.Update(ctx, user)
|
||||
}
|
||||
|
||||
// ChangeUserWorkspace 分配用户到 workspace
|
||||
func (s *UserManagementService) ChangeUserWorkspace(ctx context.Context, userID, workspaceID string) error {
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Admin 不能分配到 workspace
|
||||
if user.Role == entity.RoleAdmin {
|
||||
return fmt.Errorf("admin user cannot be assigned to workspace")
|
||||
}
|
||||
|
||||
// 验证 workspace 存在
|
||||
if workspaceID != "" {
|
||||
_, err := s.workspaceRepo.GetByID(ctx, workspaceID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows || err == entity.ErrWorkspaceNotFound {
|
||||
return entity.ErrWorkspaceNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
user.WorkspaceID = workspaceID
|
||||
return s.userRepo.Update(ctx, user)
|
||||
}
|
||||
|
||||
// ResetPassword 重置用户密码(Admin 操作)
|
||||
func (s *UserManagementService) ResetPassword(ctx context.Context, userID, newPassword string) error {
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 哈希新密码
|
||||
passwordHash, err := s.passwordHasher.Hash(newPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新密码并设置必须修改密码标志
|
||||
user.PasswordHash = passwordHash
|
||||
user.MustChangePassword = true
|
||||
user.RevokeAllTokens() // 强制登出所有会话
|
||||
|
||||
return s.userRepo.Update(ctx, user)
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户
|
||||
func (s *UserManagementService) DeleteUser(ctx context.Context, id string) error {
|
||||
return s.userRepo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
// GetUserWithWorkspace 获取用户及其 workspace 信息
|
||||
type UserWithWorkspace struct {
|
||||
User *entity.User
|
||||
Workspace *entity.Workspace
|
||||
}
|
||||
|
||||
// GetUserWithWorkspace 获取用户及其 workspace 信息
|
||||
func (s *UserManagementService) GetUserWithWorkspace(ctx context.Context, userID string) (*UserWithWorkspace, error) {
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &UserWithWorkspace{
|
||||
User: user,
|
||||
}
|
||||
|
||||
if user.WorkspaceID != "" {
|
||||
workspace, _ := s.workspaceRepo.GetByID(ctx, user.WorkspaceID)
|
||||
result.Workspace = workspace
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ListUsersWithWorkspace 列出用户及其 workspace 信息
|
||||
func (s *UserManagementService) ListUsersWithWorkspace(ctx context.Context) ([]*UserWithWorkspace, error) {
|
||||
users, err := s.userRepo.List(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 预加载所有 workspace
|
||||
workspaces, err := s.workspaceRepo.List(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
workspaceMap := make(map[string]*entity.Workspace)
|
||||
for _, w := range workspaces {
|
||||
workspaceMap[w.ID] = w
|
||||
}
|
||||
|
||||
result := make([]*UserWithWorkspace, len(users))
|
||||
for i, user := range users {
|
||||
result[i] = &UserWithWorkspace{
|
||||
User: user,
|
||||
}
|
||||
if user.WorkspaceID != "" {
|
||||
result[i].Workspace = workspaceMap[user.WorkspaceID]
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// EnsureAdminExists 确保存在一个 Admin 用户
|
||||
func (s *UserManagementService) EnsureAdminExists(ctx context.Context, defaultPassword string) error {
|
||||
users, err := s.userRepo.List(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查是否已有 admin
|
||||
for _, u := range users {
|
||||
if u.Role == entity.RoleAdmin {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 创建默认 admin 用户
|
||||
_, err = s.CreateUser(ctx, "admin", defaultPassword, "", string(entity.RoleAdmin), "")
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateInitialUser 创建初始用户(首次启动时调用)
|
||||
func (s *UserManagementService) CreateInitialUser(ctx context.Context, username, password, role string) (*entity.User, error) {
|
||||
// 检查是否已有用户
|
||||
users, err := s.userRepo.List(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(users) > 0 {
|
||||
return nil, fmt.Errorf("initial user already exists")
|
||||
}
|
||||
|
||||
// 验证角色
|
||||
if role != string(entity.RoleAdmin) && role != string(entity.RoleUser) {
|
||||
return nil, fmt.Errorf("invalid role: %s", role)
|
||||
}
|
||||
|
||||
// 哈希密码
|
||||
passwordHash, err := s.passwordHasher.Hash(password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 生成占位邮箱
|
||||
email := username + "@local.ocdp"
|
||||
|
||||
// 创建用户
|
||||
user := entity.NewUser(username, passwordHash, email)
|
||||
user.ID = uuid.New().String()
|
||||
user.Role = entity.UserRole(role)
|
||||
// workspace_id 为 NULL(admin)或空(首个普通用户)
|
||||
user.IsActive = true
|
||||
user.MustChangePassword = false // 初始用户不需要强制修改密码
|
||||
|
||||
if err := user.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 设置创建时间和更新时间
|
||||
now := time.Now()
|
||||
user.CreatedAt = now
|
||||
user.UpdatedAt = now
|
||||
|
||||
if err := s.userRepo.Create(ctx, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
143
backend/internal/domain/service/values_template_service.go
Normal file
143
backend/internal/domain/service/values_template_service.go
Normal file
@ -0,0 +1,143 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/repository"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrTemplateNotFound = errors.New("template not found")
|
||||
ErrTemplateExists = errors.New("template already exists")
|
||||
)
|
||||
|
||||
// ValuesTemplateService Values 模板领域服务
|
||||
type ValuesTemplateService struct {
|
||||
valuesTemplateRepo repository.ValuesTemplateRepository
|
||||
chartRefRepo repository.ChartReferenceRepository
|
||||
}
|
||||
|
||||
// NewValuesTemplateService 创建 Values 模板服务
|
||||
func NewValuesTemplateService(
|
||||
valuesTemplateRepo repository.ValuesTemplateRepository,
|
||||
chartRefRepo repository.ChartReferenceRepository,
|
||||
) *ValuesTemplateService {
|
||||
return &ValuesTemplateService{
|
||||
valuesTemplateRepo: valuesTemplateRepo,
|
||||
chartRefRepo: chartRefRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建 Values 模板
|
||||
func (s *ValuesTemplateService) Create(
|
||||
ctx context.Context,
|
||||
workspaceID, ownerID, chartRefID, name, description, valuesYAML string,
|
||||
isDefault bool,
|
||||
) (*entity.ValuesTemplate, error) {
|
||||
// 检查 Chart Reference 是否存在
|
||||
chartRef, err := s.chartRefRepo.GetByID(ctx, chartRefID)
|
||||
if err != nil {
|
||||
return nil, errors.New("chart reference not found")
|
||||
}
|
||||
|
||||
// 检查名称是否已存在
|
||||
existing, _ := s.valuesTemplateRepo.GetByName(ctx, workspaceID, chartRefID, name)
|
||||
if existing != nil {
|
||||
return nil, ErrTemplateExists
|
||||
}
|
||||
|
||||
template := entity.NewValuesTemplate(workspaceID, ownerID, chartRef.ID, name, valuesYAML)
|
||||
template.Description = description
|
||||
template.IsDefault = isDefault
|
||||
|
||||
if err := s.valuesTemplateRepo.Create(ctx, template); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return template, nil
|
||||
}
|
||||
|
||||
// GetByID 获取 Values 模板
|
||||
func (s *ValuesTemplateService) GetByID(ctx context.Context, id string) (*entity.ValuesTemplate, error) {
|
||||
template, err := s.valuesTemplateRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, ErrTemplateNotFound
|
||||
}
|
||||
return template, nil
|
||||
}
|
||||
|
||||
// GetByWorkspace 获取工作空间的所有 Values 模板
|
||||
func (s *ValuesTemplateService) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.ValuesTemplate, error) {
|
||||
return s.valuesTemplateRepo.GetByWorkspace(ctx, workspaceID)
|
||||
}
|
||||
|
||||
// GetByChartReference 获取 Chart Reference 的所有 Values 模板
|
||||
func (s *ValuesTemplateService) GetByChartReference(ctx context.Context, chartRefID string) ([]*entity.ValuesTemplate, error) {
|
||||
return s.valuesTemplateRepo.GetByChartReference(ctx, chartRefID)
|
||||
}
|
||||
|
||||
// GetHistory 获取模板的版本历史
|
||||
func (s *ValuesTemplateService) GetHistory(ctx context.Context, chartRefID, name string) ([]*entity.ValuesTemplate, error) {
|
||||
return s.valuesTemplateRepo.GetHistory(ctx, chartRefID, name)
|
||||
}
|
||||
|
||||
// Update 更新 Values 模板(创建新版本)
|
||||
func (s *ValuesTemplateService) Update(
|
||||
ctx context.Context,
|
||||
id, description, valuesYAML string,
|
||||
isDefault bool,
|
||||
) (*entity.ValuesTemplate, error) {
|
||||
template, err := s.valuesTemplateRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, ErrTemplateNotFound
|
||||
}
|
||||
|
||||
template.Description = description
|
||||
template.ValuesYAML = valuesYAML
|
||||
template.IsDefault = isDefault
|
||||
|
||||
if err := s.valuesTemplateRepo.Update(ctx, template); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取最新版本
|
||||
return s.valuesTemplateRepo.GetByName(ctx, template.WorkspaceID, template.ChartReferenceID, template.Name)
|
||||
}
|
||||
|
||||
// Delete 删除 Values 模板
|
||||
func (s *ValuesTemplateService) Delete(ctx context.Context, id string) error {
|
||||
return s.valuesTemplateRepo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
// List 列出所有 Values 模板(管理员用)
|
||||
func (s *ValuesTemplateService) List(ctx context.Context) ([]*entity.ValuesTemplate, error) {
|
||||
return s.valuesTemplateRepo.List(ctx)
|
||||
}
|
||||
|
||||
// Rollback 回滚到指定版本
|
||||
func (s *ValuesTemplateService) Rollback(ctx context.Context, templateID string) (*entity.ValuesTemplate, error) {
|
||||
// 获取历史版本模板
|
||||
oldTemplate, err := s.valuesTemplateRepo.GetByID(ctx, templateID)
|
||||
if err != nil {
|
||||
return nil, ErrTemplateNotFound
|
||||
}
|
||||
|
||||
// 重新创建该版本(创建新版本,内容与旧版本相同)
|
||||
newTemplate := &entity.ValuesTemplate{
|
||||
WorkspaceID: oldTemplate.WorkspaceID,
|
||||
OwnerID: oldTemplate.OwnerID,
|
||||
ChartReferenceID: oldTemplate.ChartReferenceID,
|
||||
Name: oldTemplate.Name,
|
||||
Description: oldTemplate.Description,
|
||||
ValuesYAML: oldTemplate.ValuesYAML,
|
||||
}
|
||||
|
||||
if err := s.valuesTemplateRepo.Update(ctx, newTemplate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取最新版本
|
||||
return s.valuesTemplateRepo.GetByName(ctx, newTemplate.WorkspaceID, newTemplate.ChartReferenceID, newTemplate.Name)
|
||||
}
|
||||
121
backend/internal/domain/service/workspace_service.go
Normal file
121
backend/internal/domain/service/workspace_service.go
Normal file
@ -0,0 +1,121 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/repository"
|
||||
)
|
||||
|
||||
// WorkspaceService 工作空间领域服务
|
||||
type WorkspaceService struct {
|
||||
workspaceRepo repository.WorkspaceRepository
|
||||
quotaRepo repository.QuotaRepository
|
||||
userRepo repository.UserRepository
|
||||
}
|
||||
|
||||
// NewWorkspaceService 创建工作空间服务
|
||||
func NewWorkspaceService(
|
||||
workspaceRepo repository.WorkspaceRepository,
|
||||
quotaRepo repository.QuotaRepository,
|
||||
userRepo repository.UserRepository,
|
||||
) *WorkspaceService {
|
||||
return &WorkspaceService{
|
||||
workspaceRepo: workspaceRepo,
|
||||
quotaRepo: quotaRepo,
|
||||
userRepo: userRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建工作空间
|
||||
func (s *WorkspaceService) Create(ctx context.Context, name, description, createdBy string) (*entity.Workspace, error) {
|
||||
// 检查名称是否已存在
|
||||
existing, _ := s.workspaceRepo.GetByName(ctx, name)
|
||||
if existing != nil {
|
||||
return nil, entity.ErrWorkspaceExists
|
||||
}
|
||||
|
||||
workspace := entity.NewWorkspace(name, description, createdBy)
|
||||
if err := s.workspaceRepo.Create(ctx, workspace); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return workspace, nil
|
||||
}
|
||||
|
||||
// GetByID 获取工作空间
|
||||
func (s *WorkspaceService) GetByID(ctx context.Context, id string) (*entity.Workspace, error) {
|
||||
return s.workspaceRepo.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
// GetByName 获取工作空间
|
||||
func (s *WorkspaceService) GetByName(ctx context.Context, name string) (*entity.Workspace, error) {
|
||||
return s.workspaceRepo.GetByName(ctx, name)
|
||||
}
|
||||
|
||||
// Update 更新工作空间
|
||||
func (s *WorkspaceService) Update(ctx context.Context, workspace *entity.Workspace) error {
|
||||
return s.workspaceRepo.Update(ctx, workspace)
|
||||
}
|
||||
|
||||
// Delete 删除工作空间
|
||||
func (s *WorkspaceService) Delete(ctx context.Context, id string) error {
|
||||
// 删除关联的配额
|
||||
if err := s.quotaRepo.DeleteByWorkspace(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.workspaceRepo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
// List 列出所有工作空间
|
||||
func (s *WorkspaceService) List(ctx context.Context) ([]*entity.Workspace, error) {
|
||||
return s.workspaceRepo.List(ctx)
|
||||
}
|
||||
|
||||
// GetQuotas 获取工作空间配额
|
||||
func (s *WorkspaceService) GetQuotas(ctx context.Context, workspaceID string) ([]*entity.WorkspaceQuota, error) {
|
||||
return s.quotaRepo.GetByWorkspace(ctx, workspaceID)
|
||||
}
|
||||
|
||||
// SetQuota 设置配额
|
||||
func (s *WorkspaceService) SetQuota(ctx context.Context, workspaceID string, resourceType entity.ResourceType, hardLimit, softLimit float64) (*entity.WorkspaceQuota, error) {
|
||||
quota := entity.NewWorkspaceQuota(workspaceID, resourceType, hardLimit, softLimit)
|
||||
if err := s.quotaRepo.Create(ctx, quota); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return quota, nil
|
||||
}
|
||||
|
||||
// SetQuotas 批量设置配额
|
||||
func (s *WorkspaceService) SetQuotas(ctx context.Context, workspaceID string, quotas map[entity.ResourceType]struct {
|
||||
HardLimit float64
|
||||
SoftLimit float64
|
||||
}) error {
|
||||
for resourceType, config := range quotas {
|
||||
quota := entity.NewWorkspaceQuota(workspaceID, resourceType, config.HardLimit, config.SoftLimit)
|
||||
if err := s.quotaRepo.Create(ctx, quota); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOrCreateDefaultQuota 获取或创建默认配额
|
||||
func (s *WorkspaceService) GetOrCreateDefaultQuota(ctx context.Context, workspaceID string, resourceType entity.ResourceType) (*entity.WorkspaceQuota, error) {
|
||||
quota, _ := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, resourceType)
|
||||
if quota != nil {
|
||||
return quota, nil
|
||||
}
|
||||
|
||||
// 创建默认配额(无限制)
|
||||
quota = entity.NewWorkspaceQuota(workspaceID, resourceType, 0, 0)
|
||||
if err := s.quotaRepo.Create(ctx, quota); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return quota, nil
|
||||
}
|
||||
|
||||
// GetUsers 获取工作空间的用户
|
||||
func (s *WorkspaceService) GetUsers(ctx context.Context, workspaceID string) ([]*entity.User, error) {
|
||||
return s.userRepo.ListByWorkspace(ctx, workspaceID)
|
||||
}
|
||||
Reference in New Issue
Block a user