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:
Ivan087
2026-04-15 16:59:31 +08:00
parent c5e51ed069
commit 29d0310f03
283 changed files with 24658 additions and 36038 deletions

View File

@ -0,0 +1,90 @@
package entity
import (
"encoding/json"
"time"
)
// AuditAction 审计操作类型
type AuditAction string
const (
AuditActionCreate AuditAction = "create"
AuditActionUpdate AuditAction = "update"
AuditActionDelete AuditAction = "delete"
AuditActionDeploy AuditAction = "deploy"
AuditActionScale AuditAction = "scale"
AuditActionLogin AuditAction = "login"
AuditActionLogout AuditAction = "logout"
AuditActionChangePassword AuditAction = "change_password"
)
// AuditResourceType 审计资源类型
type AuditResourceType string
const (
AuditResourceUser AuditResourceType = "user"
AuditResourceWorkspace AuditResourceType = "workspace"
AuditResourceQuota AuditResourceType = "quota"
AuditResourceCluster AuditResourceType = "cluster"
AuditResourceRegistry AuditResourceType = "registry"
AuditResourceInstance AuditResourceType = "instance"
AuditResourceStorage AuditResourceType = "storage"
AuditResourceTemplate AuditResourceType = "template"
)
// AuditLog 审计日志实体
type AuditLog struct {
ID string
WorkspaceID string
UserID string
Action AuditAction
ResourceType AuditResourceType
ResourceID string
ResourceName string
Details map[string]interface{}
IPAddress string
UserAgent string
CreatedAt time.Time
}
// NewAuditLog 创建新审计日志
func NewAuditLog(workspaceID, userID string, action AuditAction, resourceType AuditResourceType) *AuditLog {
now := time.Now()
return &AuditLog{
WorkspaceID: workspaceID,
UserID: userID,
Action: action,
ResourceType: resourceType,
CreatedAt: now,
}
}
// SetResource 设置关联资源
func (a *AuditLog) SetResource(resourceID, resourceName string) {
a.ResourceID = resourceID
a.ResourceName = resourceName
}
// SetDetails 设置详细信息
func (a *AuditLog) SetDetails(details map[string]interface{}) {
a.Details = details
}
// SetClientInfo 设置客户端信息
func (a *AuditLog) SetClientInfo(ipAddress, userAgent string) {
a.IPAddress = ipAddress
a.UserAgent = userAgent
}
// DetailsJSON 将详情转为 JSON 字符串
func (a *AuditLog) DetailsJSON() (string, error) {
if a.Details == nil {
return "{}", nil
}
data, err := json.Marshal(a.Details)
if err != nil {
return "", err
}
return string(data), nil
}

View File

@ -0,0 +1,41 @@
package entity
import (
"time"
)
// ChartReference Chart 引用实体
type ChartReference struct {
ID string
WorkspaceID string
RegistryID string
Repository string
ChartName string
Description string
IsEnabled bool
CreatedAt time.Time
UpdatedAt time.Time
}
// NewChartReference 创建新 Chart 引用
func NewChartReference(workspaceID, registryID, repository, chartName, description string) *ChartReference {
now := time.Now()
return &ChartReference{
WorkspaceID: workspaceID,
RegistryID: registryID,
Repository: repository,
ChartName: chartName,
Description: description,
IsEnabled: true,
CreatedAt: now,
UpdatedAt: now,
}
}
// Validate 验证 Chart 引用数据
func (c *ChartReference) Validate() error {
if c.Repository == "" {
return ErrInvalidChart
}
return nil
}

View File

@ -4,28 +4,49 @@ import (
"time"
)
// IsolationMode 集群隔离模式
type IsolationMode string
const (
IsolationModeNamespace IsolationMode = "namespace" // 共享集群模式,多 workspace 使用不同 namespace
IsolationModeCluster IsolationMode = "cluster" // 私有集群模式,每个 workspace 独立集群
)
// Cluster Kubernetes 集群领域实体
type Cluster struct {
ID string
Name string
Host string // Kubernetes API Server URL
CAData string // Base64 encoded CA certificate
CertData string // Base64 encoded client certificate
KeyData string // Base64 encoded client key
Token string // Bearer token (alternative to cert auth)
Description string
CreatedAt time.Time
UpdatedAt time.Time
ID string
WorkspaceID string // 所属 workspaceNULL 表示全局共享
OwnerID string // 创建者用户 ID
Name string
Host string // Kubernetes API Server URL
CAData string // Base64 encoded CA certificate
CertData string // Base64 encoded client certificate
KeyData string // Base64 encoded client key
Token string // Bearer token (alternative to cert auth)
Description string
// 隔离模式
IsolationMode IsolationMode // 'namespace' | 'cluster'
DefaultNamespace string // 当 isolation_mode=namespace 时的默认 namespace 前缀
IsShared bool // 是否为共享集群admin 创建供多 workspace 使用)
CreatedAt time.Time
UpdatedAt time.Time
}
// NewCluster 创建新集群
func NewCluster(name, host string) *Cluster {
func NewCluster(workspaceID, ownerID, name, host string) *Cluster {
now := time.Now()
return &Cluster{
Name: name,
Host: host,
CreatedAt: now,
UpdatedAt: now,
WorkspaceID: workspaceID,
OwnerID: ownerID,
Name: name,
Host: host,
IsolationMode: IsolationModeNamespace, // 默认 namespace 隔离模式
DefaultNamespace: workspaceID, // 默认使用 workspace ID 作为 namespace 前缀
IsShared: false,
CreatedAt: now,
UpdatedAt: now,
}
}
@ -63,11 +84,35 @@ func (c *Cluster) Validate() error {
if c.Host == "" {
return ErrInvalidClusterHost
}
// 必须有认证方式:证书或 Token
if (c.CertData == "" || c.KeyData == "") && c.Token == "" {
return ErrInvalidClusterAuth
// 检查是否有 kubeconfig 格式(完整的 kubeconfig 在 CAData 中)
hasKubeconfig := len(c.CAData) > 100 && (c.CAData[:11] == "apiVersion:" || c.CAData[:5] == "kind:")
// 认证方式证书、Token、kubeconfig 或空(使用本地 kubeconfig
hasCertAuth := c.CertData != "" && c.KeyData != ""
hasToken := c.Token != ""
hasNoAuth := c.CertData == "" && c.KeyData == "" && c.Token == ""
// 如果有 kubeconfig 格式,或有证书,或有 token或没有凭证依赖 TestConnection 使用本地 kubeconfig都是有效的
if hasKubeconfig || hasCertAuth || hasToken || hasNoAuth {
return nil
}
return nil
return ErrInvalidClusterAuth
}
// GetNamespace 获取部署用的 namespace
// namespace 隔离模式: {workspace_id}-{instance_name} 或 {default_namespace}-{username}
// cluster 隔离模式: 使用 workspace 的默认 namespace
func (c *Cluster) GetNamespace(workspaceName, instanceName string) string {
if c.IsolationMode == IsolationModeCluster {
return c.DefaultNamespace
}
// namespace 隔离模式
if c.DefaultNamespace != "" {
return c.DefaultNamespace + "-" + instanceName
}
return workspaceName + "-" + instanceName
}
// GetKubeConfig 生成 kubeconfig 内容

View File

@ -37,4 +37,32 @@ var (
ErrArtifactNotFound = errors.New("artifact not found")
ErrRepositoryNotFound = errors.New("repository not found")
ErrValuesSchemaNotFound = errors.New("values schema not found")
ErrValuesNotFound = errors.New("values not found")
// Workspace errors
ErrInvalidWorkspaceName = errors.New("invalid workspace name")
ErrWorkspaceNotFound = errors.New("workspace not found")
ErrWorkspaceExists = errors.New("workspace already exists")
// Quota errors
ErrQuotaExceeded = errors.New("quota exceeded")
ErrInvalidQuota = errors.New("invalid quota")
// Storage errors
ErrInvalidStorageName = errors.New("invalid storage name")
ErrStorageNotFound = errors.New("storage not found")
ErrStorageExists = errors.New("storage already exists")
// Chart Reference errors
ErrInvalidChartReferenceName = errors.New("invalid chart reference name")
ErrChartReferenceNotFound = errors.New("chart reference not found")
ErrChartReferenceExists = errors.New("chart reference already exists")
// Template errors
ErrInvalidTemplateName = errors.New("invalid template name")
ErrTemplateNotFound = errors.New("template not found")
ErrTemplateExists = errors.New("template already exists")
// Permission errors
ErrPermissionDenied = errors.New("permission denied")
)

View File

@ -1,7 +1,9 @@
package entity
import (
"strings"
"time"
"unicode"
)
// InstanceStatus 实例状态
@ -33,43 +35,65 @@ const (
// Instance Helm 应用实例领域实体
type Instance struct {
ID string
ClusterID string
Name string // Helm Release Name
Namespace string
RegistryID string
Repository string // OCI Repository (e.g., charts/app)
Chart string // Chart Name
Version string // Chart Version
Description string
Values map[string]interface{} // Helm Values (JSON)
ValuesYAML string // Helm Values (YAML format)
Status InstanceStatus
StatusReason string
LastOperation InstanceOperation
LastError string
Revision int // Helm Release Revision
CreatedAt time.Time
UpdatedAt time.Time
ID string
WorkspaceID string // 所属 workspace
OwnerID string // 创建者用户 ID
ClusterID string
RegistryID string
ChartReferenceID string // 引用的 Chart 引用
ValuesTemplateID string // 使用的 Values 模板
Name string // Helm Release Name
Namespace string
Repository string // OCI Repository (e.g., charts/app)
Chart string // Chart Name
Version string // Chart Version
Description string
Values map[string]interface{} // Helm Values (JSON)
ValuesYAML string // Helm Values (YAML format)
UserOverrideYAML string // 用户额外覆盖的配置
Status InstanceStatus
StatusReason string
LastOperation InstanceOperation
LastError string
Revision int // Helm Release Revision
// 资源使用统计Helm 安装时从集群获取并更新)
CPURequested float64 // CPU 请求量 (cores)
MemoryRequested string // 内存请求量 (e.g., "2Gi")
GPURequested float64 // GPU 请求量 (cards)
GPUMemoryRequested string // GPU 内存请求量 (e.g., "16Gi")
CreatedAt time.Time
UpdatedAt time.Time
}
// NewInstance 创建新实例
func NewInstance(clusterID, name, namespace, registryID, repository, chart, version string) *Instance {
func NewInstance(workspaceID, ownerID, clusterID, registryID, chartReferenceID, valuesTemplateID, name, namespace, repository, chart, version string) *Instance {
now := time.Now()
return &Instance{
ClusterID: clusterID,
Name: name,
Namespace: namespace,
RegistryID: registryID,
Repository: repository,
Chart: chart,
Version: version,
Status: StatusPending,
StatusReason: "Pending install",
LastOperation: OperationInstall,
Revision: 1,
CreatedAt: now,
UpdatedAt: now,
WorkspaceID: workspaceID,
OwnerID: ownerID,
ClusterID: clusterID,
RegistryID: registryID,
ChartReferenceID: chartReferenceID,
ValuesTemplateID: valuesTemplateID,
Name: name,
Namespace: namespace,
Repository: repository,
Chart: chart,
Version: version,
Status: StatusPending,
StatusReason: "Pending install",
LastOperation: OperationInstall,
Revision: 1,
CPURequested: 0,
MemoryRequested: "0Mi",
GPURequested: 0,
GPUMemoryRequested: "0Mi",
CreatedAt: now,
UpdatedAt: now,
}
}
@ -154,13 +178,43 @@ func (i *Instance) Upgrade(version string, values map[string]interface{}) {
i.BeginOperation(OperationUpgrade, "Pending upgrade")
}
// ValidateReleaseName 验证 Helm Release 名称是否符合 RFC 1123 DNS 子域名规范
// Helm release 名称必须:
// - 只能包含小写字母a-z、数字0-9和连字符-
// - 不能以连字符开头或结尾
// - 长度不超过 53 个字符
func ValidateReleaseName(name string) error {
if name == "" {
return ErrInvalidInstanceName
}
// 检查长度RFC 1123 DNS 子域名最大长度为 63但 Helm 限制为 53
if len(name) > 53 {
return ErrInvalidInstanceName
}
// 不能以连字符开头或结尾
if strings.HasPrefix(name, "-") || strings.HasSuffix(name, "-") {
return ErrInvalidInstanceName
}
// 只能包含小写字母、数字和连字符
for _, r := range name {
if !(unicode.IsLower(r) || unicode.IsDigit(r) || r == '-') {
return ErrInvalidInstanceName
}
}
return nil
}
// Validate 验证实例配置
func (i *Instance) Validate() error {
if i.ClusterID == "" {
return ErrInvalidClusterID
}
if i.Name == "" {
return ErrInvalidInstanceName
if err := ValidateReleaseName(i.Name); err != nil {
return err
}
if i.Namespace == "" {
return ErrInvalidNamespace

View File

@ -0,0 +1,79 @@
package entity
import (
"time"
)
// ResourceType 资源类型
type ResourceType string
const (
ResourceCPU ResourceType = "cpu"
ResourceGPU ResourceType = "gpu"
ResourceGPUMemory ResourceType = "gpu_memory"
)
// WorkspaceQuota 工作空间配额实体
type WorkspaceQuota struct {
ID string
WorkspaceID string
ResourceType ResourceType
HardLimit float64 // 硬限制0表示无限制
SoftLimit float64 // 软限制(警告阈值)
Used float64 // 当前使用量
CreatedAt time.Time
UpdatedAt time.Time
}
// NewWorkspaceQuota 创建新配额
func NewWorkspaceQuota(workspaceID string, resourceType ResourceType, hardLimit, softLimit float64) *WorkspaceQuota {
now := time.Now()
return &WorkspaceQuota{
WorkspaceID: workspaceID,
ResourceType: resourceType,
HardLimit: hardLimit,
SoftLimit: softLimit,
Used: 0,
CreatedAt: now,
UpdatedAt: now,
}
}
// CanAllocate 检查是否可以分配指定资源量
func (q *WorkspaceQuota) CanAllocate(amount float64) bool {
if q.HardLimit == 0 {
return true // 无限制
}
return q.Used+amount <= q.HardLimit
}
// Allocate 分配资源
func (q *WorkspaceQuota) Allocate(amount float64) {
q.Used += amount
q.UpdatedAt = time.Now()
}
// Release 释放资源
func (q *WorkspaceQuota) Release(amount float64) {
q.Used -= amount
if q.Used < 0 {
q.Used = 0
}
q.UpdatedAt = time.Now()
}
// IsOverLimit 检查是否超过硬限制
func (q *WorkspaceQuota) IsOverLimit() bool {
if q.HardLimit == 0 {
return false
}
return q.Used > q.HardLimit
}
// IsOverSoftLimit 检查是否超过软限制
func (q *WorkspaceQuota) IsOverSoftLimit() bool {
if q.SoftLimit == 0 {
return false
}
return q.Used > q.SoftLimit
}

View File

@ -7,24 +7,30 @@ import (
// Registry OCI Registry 领域实体
type Registry struct {
ID string
WorkspaceID string // 所属 workspaceNULL 表示全局共享
OwnerID string // 创建者用户 ID
Name string
URL string
Description string
Username string
Password string
Insecure bool // 是否跳过 TLS 验证
Insecure bool // 是否跳过 TLS 验证
IsShared bool // 是否为共享 Registryadmin 创建供多 workspace 使用)
CreatedAt time.Time
UpdatedAt time.Time
}
// NewRegistry 创建新 Registry
func NewRegistry(name, url string) *Registry {
func NewRegistry(workspaceID, ownerID, name, url string) *Registry {
now := time.Now()
return &Registry{
Name: name,
URL: url,
CreatedAt: now,
UpdatedAt: now,
WorkspaceID: workspaceID,
OwnerID: ownerID,
Name: name,
URL: url,
IsShared: false,
CreatedAt: now,
UpdatedAt: now,
}
}

View File

@ -0,0 +1,98 @@
package entity
import (
"encoding/json"
"time"
)
// StorageType 存储类型
type StorageType string
const (
StorageTypeNFS StorageType = "nfs"
StorageTypePV StorageType = "pv"
StorageTypeHostPath StorageType = "hostPath"
)
// StorageConfig 存储配置
type StorageConfig struct {
NFS *NFSConfig `json:"nfs,omitempty"`
PV *PVConfig `json:"pv,omitempty"`
HostPath *HostPathConfig `json:"hostPath,omitempty"`
}
// NFSConfig NFS 配置
type NFSConfig struct {
Server string `json:"server"`
Path string `json:"path"`
}
// PVConfig PV 配置
type PVConfig struct {
StorageClassName string `json:"storageClassName"`
Capacity string `json:"capacity"`
AccessModes []string `json:"accessModes"`
}
// HostPathConfig HostPath 配置
type HostPathConfig struct {
Path string `json:"path"`
}
// StorageBackend 存储后端实体
type StorageBackend struct {
ID string
WorkspaceID string
OwnerID string
Name string
Type StorageType
Config StorageConfig
Description string
IsDefault bool
IsShared bool
CreatedAt time.Time
UpdatedAt time.Time
}
// NewStorageBackend 创建新存储后端
func NewStorageBackend(workspaceID, ownerID, name string, storageType StorageType, config StorageConfig) *StorageBackend {
now := time.Now()
return &StorageBackend{
WorkspaceID: workspaceID,
OwnerID: ownerID,
Name: name,
Type: storageType,
Config: config,
IsDefault: false,
IsShared: false,
CreatedAt: now,
UpdatedAt: now,
}
}
// Validate 验证存储后端数据
func (s *StorageBackend) Validate() error {
if s.Name == "" {
return ErrInvalidStorageName
}
if s.Type == "" {
return ErrInvalidStorageName
}
return nil
}
// ConfigJSON 将配置转为 JSON 字符串
func (s *StorageBackend) ConfigJSON() (string, error) {
data, err := json.Marshal(s.Config)
if err != nil {
return "", err
}
return string(data), nil
}
// ParseConfigJSON 从 JSON 解析配置
func ParseConfigJSON(jsonStr string) (*StorageConfig, error) {
var config StorageConfig
err := json.Unmarshal([]byte(jsonStr), &config)
return &config, err
}

View File

@ -4,30 +4,58 @@ import (
"time"
)
// UserRole 用户角色
type UserRole string
const (
RoleAdmin UserRole = "admin"
RoleUser UserRole = "user"
)
// User 用户领域实体
type User struct {
ID string
Username string
PasswordHash string
Email string
RevokedAfter time.Time // 全局 Token 撤销时间
CreatedAt time.Time
UpdatedAt time.Time
ID string
Username string
PasswordHash string
Email string
Role UserRole // 用户角色: admin, user
WorkspaceID string // 所属工作空间admin 为空表示全局
IsActive bool // 账户是否激活
MustChangePassword bool // 首次登录必须修改密码
RevokedAfter time.Time // 全局 Token 撤销时间
CreatedAt time.Time
UpdatedAt time.Time
}
// NewUser 创建新用户
func NewUser(username, passwordHash, email string) *User {
now := time.Now()
return &User{
Username: username,
PasswordHash: passwordHash,
Email: email,
RevokedAfter: time.Unix(0, 0), // 初始值1970-01-01
CreatedAt: now,
UpdatedAt: now,
Username: username,
PasswordHash: passwordHash,
Email: email,
Role: RoleUser, // 默认普通用户
IsActive: true,
MustChangePassword: true, // 首次登录必须修改密码
RevokedAfter: time.Unix(0, 0), // 初始值1970-01-01
CreatedAt: now,
UpdatedAt: now,
}
}
// IsAdmin 判断是否为管理员
func (u *User) IsAdmin() bool {
return u.Role == RoleAdmin
}
// CanAccessWorkspace 检查是否可以访问指定工作空间
func (u *User) CanAccessWorkspace(workspaceID string) bool {
if u.IsAdmin() {
return true // Admin 可以访问所有工作空间
}
return u.WorkspaceID == workspaceID
}
// UpdatePassword 更新密码(会触发全局登出)
func (u *User) UpdatePassword(newPasswordHash string) {
u.PasswordHash = newPasswordHash

View File

@ -0,0 +1,83 @@
package entity
import (
"time"
)
// ValuesTemplate Values 模板实体(带版本管理)
type ValuesTemplate struct {
ID string
WorkspaceID string
OwnerID string
ChartReferenceID string
Name string
Description string
ValuesYAML string
Version int // 模板版本号
IsDefault bool
CreatedAt time.Time
UpdatedAt time.Time
}
// NewValuesTemplate 创建新 Values 模板
func NewValuesTemplate(workspaceID, ownerID, chartReferenceID, name, valuesYAML string) *ValuesTemplate {
now := time.Now()
return &ValuesTemplate{
WorkspaceID: workspaceID,
OwnerID: ownerID,
ChartReferenceID: chartReferenceID,
Name: name,
ValuesYAML: valuesYAML,
Version: 1,
IsDefault: false,
CreatedAt: now,
UpdatedAt: now,
}
}
// Validate 验证 Values 模板数据
func (v *ValuesTemplate) Validate() error {
if v.Name == "" {
return ErrInvalidTemplateName
}
if v.ValuesYAML == "" {
return ErrInvalidTemplateName
}
return nil
}
// IncrementVersion 递增版本号
func (v *ValuesTemplate) IncrementVersion() {
v.Version++
v.UpdatedAt = time.Now()
}
// UserConfigOverride 用户配置覆盖实体
type UserConfigOverride struct {
ID string
WorkspaceID string
UserID string
TargetType string // 'storage', 'template', 'global'
TargetID string
Config map[string]interface{}
Priority int
IsActive bool
CreatedAt time.Time
UpdatedAt time.Time
}
// NewUserConfigOverride 创建新用户配置覆盖
func NewUserConfigOverride(workspaceID, userID, targetType, targetID string, config map[string]interface{}) *UserConfigOverride {
now := time.Now()
return &UserConfigOverride{
WorkspaceID: workspaceID,
UserID: userID,
TargetType: targetType,
TargetID: targetID,
Config: config,
Priority: 0,
IsActive: true,
CreatedAt: now,
UpdatedAt: now,
}
}

View File

@ -0,0 +1,35 @@
package entity
import (
"time"
)
// Workspace 工作空间实体
type Workspace struct {
ID string
Name string
Description string
CreatedBy string // 创建者用户 ID
CreatedAt time.Time
UpdatedAt time.Time
}
// NewWorkspace 创建新工作空间
func NewWorkspace(name, description, createdBy string) *Workspace {
now := time.Now()
return &Workspace{
Name: name,
Description: description,
CreatedBy: createdBy,
CreatedAt: now,
UpdatedAt: now,
}
}
// Validate 验证工作空间数据
func (w *Workspace) Validate() error {
if w.Name == "" {
return ErrInvalidWorkspaceName
}
return nil
}

View File

@ -0,0 +1,27 @@
package repository
import (
"context"
"github.com/ocdp/cluster-service/internal/domain/entity"
)
// AuditLogRepository 审计日志仓储接口
type AuditLogRepository interface {
// Create 创建审计日志
Create(ctx context.Context, log *entity.AuditLog) error
// GetByWorkspace 获取 workspace 的审计日志
GetByWorkspace(ctx context.Context, workspaceID string, limit int) ([]*entity.AuditLog, error)
// GetByUser 获取用户的审计日志
GetByUser(ctx context.Context, userID string, limit int) ([]*entity.AuditLog, error)
// GetByResource 获取资源的审计日志
GetByResource(ctx context.Context, resourceType entity.AuditResourceType, resourceID string, limit int) ([]*entity.AuditLog, error)
// List 列出审计日志(分页)
List(ctx context.Context, limit, offset int) ([]*entity.AuditLog, error)
// DeleteByWorkspace 删除 workspace 的审计日志
DeleteByWorkspace(ctx context.Context, workspaceID string) error
}

View File

@ -0,0 +1,33 @@
package repository
import (
"context"
"github.com/ocdp/cluster-service/internal/domain/entity"
)
// ChartReferenceRepository Chart 引用仓储接口
type ChartReferenceRepository interface {
// Create 创建 Chart 引用
Create(ctx context.Context, chartRef *entity.ChartReference) error
// GetByID 根据 ID 获取 Chart 引用
GetByID(ctx context.Context, id string) (*entity.ChartReference, error)
// GetByWorkspace 获取 workspace 的所有 Chart 引用
GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.ChartReference, error)
// GetByRegistry 获取 registry 的所有 Chart 引用
GetByRegistry(ctx context.Context, registryID string) ([]*entity.ChartReference, error)
// GetByName 根据名称获取 Chart 引用
GetByName(ctx context.Context, workspaceID, chartName string) (*entity.ChartReference, error)
// Update 更新 Chart 引用
Update(ctx context.Context, chartRef *entity.ChartReference) error
// Delete 删除 Chart 引用
Delete(ctx context.Context, id string) error
// List 列出所有 Chart 引用(管理员用)
List(ctx context.Context) ([]*entity.ChartReference, error)
}

View File

@ -9,20 +9,26 @@ import (
type ClusterRepository interface {
// Create 创建集群
Create(ctx context.Context, cluster *entity.Cluster) error
// GetByID 根据 ID 获取集群
GetByID(ctx context.Context, id string) (*entity.Cluster, error)
// GetByName 根据名称获取集群
GetByName(ctx context.Context, name string) (*entity.Cluster, error)
// Update 更新集群
Update(ctx context.Context, cluster *entity.Cluster) error
// Delete 删除集群
Delete(ctx context.Context, id string) error
// List 列出所有集群
List(ctx context.Context) ([]*entity.Cluster, error)
// GetByWorkspace 获取 workspace 的所有集群(包括共享集群)
GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.Cluster, error)
// GetShared 获取所有共享集群
GetShared(ctx context.Context) ([]*entity.Cluster, error)
}

View File

@ -9,23 +9,26 @@ import (
type InstanceRepository interface {
// Create 创建实例
Create(ctx context.Context, instance *entity.Instance) error
// GetByID 根据 ID 获取实例
GetByID(ctx context.Context, id string) (*entity.Instance, error)
// GetByClusterAndName 根据集群 ID 和名称获取实例
GetByClusterAndName(ctx context.Context, clusterID, name string) (*entity.Instance, error)
// Update 更新实例
Update(ctx context.Context, instance *entity.Instance) error
// Delete 删除实例
Delete(ctx context.Context, id string) error
// ListByCluster 列出指定集群的所有实例
ListByCluster(ctx context.Context, clusterID string) ([]*entity.Instance, error)
// List 列出所有实例
List(ctx context.Context) ([]*entity.Instance, error)
// GetByWorkspace 列出指定工作空间的所有实例(用于配额检查)
GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.Instance, error)
}

View File

@ -19,7 +19,10 @@ type OCIClient interface {
// GetValuesSchema 获取 Helm Chart 的 values schema
GetValuesSchema(ctx context.Context, registry *entity.Registry, repository, reference string) (string, error)
// GetValues 获取 Helm Chart 的 values.yaml
GetValues(ctx context.Context, registry *entity.Registry, repository, reference string) (string, error)
// PullArtifact 下载 artifact 到本地
PullArtifact(ctx context.Context, registry *entity.Registry, repository, reference, destPath string) error

View File

@ -0,0 +1,30 @@
package repository
import (
"context"
"github.com/ocdp/cluster-service/internal/domain/entity"
)
// QuotaRepository 配额仓储接口
type QuotaRepository interface {
// Create 创建配额
Create(ctx context.Context, quota *entity.WorkspaceQuota) error
// GetByID 根据 ID 获取配额
GetByID(ctx context.Context, id string) (*entity.WorkspaceQuota, error)
// GetByWorkspaceAndType 根据 workspace 和资源类型获取配额
GetByWorkspaceAndType(ctx context.Context, workspaceID string, resourceType entity.ResourceType) (*entity.WorkspaceQuota, error)
// GetByWorkspace 获取 workspace 的所有配额
GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.WorkspaceQuota, error)
// Update 更新配额
Update(ctx context.Context, quota *entity.WorkspaceQuota) error
// Delete 删除配额
Delete(ctx context.Context, id string) error
// DeleteByWorkspace 删除 workspace 的所有配额
DeleteByWorkspace(ctx context.Context, workspaceID string) error
}

View File

@ -0,0 +1,36 @@
package repository
import (
"context"
"github.com/ocdp/cluster-service/internal/domain/entity"
)
// StorageRepository 存储后端仓储接口
type StorageRepository interface {
// Create 创建存储后端
Create(ctx context.Context, storage *entity.StorageBackend) error
// GetByID 根据 ID 获取存储后端
GetByID(ctx context.Context, id string) (*entity.StorageBackend, error)
// GetByName 根据名称获取存储后端
GetByName(ctx context.Context, workspaceID, name string) (*entity.StorageBackend, error)
// GetByWorkspace 获取 workspace 的所有存储后端
GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.StorageBackend, error)
// GetShared 获取所有共享存储后端
GetShared(ctx context.Context) ([]*entity.StorageBackend, error)
// GetDefault 获取 workspace 的默认存储后端
GetDefault(ctx context.Context, workspaceID string) (*entity.StorageBackend, error)
// Update 更新存储后端
Update(ctx context.Context, storage *entity.StorageBackend) error
// Delete 删除存储后端
Delete(ctx context.Context, id string) error
// List 列出所有存储后端(管理员用)
List(ctx context.Context) ([]*entity.StorageBackend, error)
}

View File

@ -9,20 +9,26 @@ import (
type UserRepository interface {
// Create 创建用户
Create(ctx context.Context, user *entity.User) error
// GetByID 根据 ID 获取用户
GetByID(ctx context.Context, id string) (*entity.User, error)
// GetByUsername 根据用户名获取用户
GetByUsername(ctx context.Context, username string) (*entity.User, error)
// Update 更新用户
Update(ctx context.Context, user *entity.User) error
// Delete 删除用户
Delete(ctx context.Context, id string) error
// List 列出所有用户
List(ctx context.Context) ([]*entity.User, error)
// ListByWorkspace 列出指定 workspace 的用户
ListByWorkspace(ctx context.Context, workspaceID string) ([]*entity.User, error)
// ListActive 仅列出活跃用户
ListActive(ctx context.Context) ([]*entity.User, error)
}

View File

@ -0,0 +1,36 @@
package repository
import (
"context"
"github.com/ocdp/cluster-service/internal/domain/entity"
)
// ValuesTemplateRepository Values 模板仓储接口
type ValuesTemplateRepository interface {
// Create 创建 Values 模板
Create(ctx context.Context, template *entity.ValuesTemplate) error
// GetByID 根据 ID 获取 Values 模板
GetByID(ctx context.Context, id string) (*entity.ValuesTemplate, error)
// GetByWorkspace 获取 workspace 的所有 Values 模板
GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.ValuesTemplate, error)
// GetByChartReference 获取 Chart Reference 的所有 Values 模板
GetByChartReference(ctx context.Context, chartRefID string) ([]*entity.ValuesTemplate, error)
// GetByName 根据名称获取 Values 模板
GetByName(ctx context.Context, workspaceID, chartRefID, name string) (*entity.ValuesTemplate, error)
// GetHistory 获取模板的版本历史
GetHistory(ctx context.Context, chartRefID, name string) ([]*entity.ValuesTemplate, error)
// Update 更新 Values 模板(自动递增版本)
Update(ctx context.Context, template *entity.ValuesTemplate) error
// Delete 删除 Values 模板
Delete(ctx context.Context, id string) error
// List 列出所有 Values 模板(管理员用)
List(ctx context.Context) ([]*entity.ValuesTemplate, error)
}

View File

@ -0,0 +1,27 @@
package repository
import (
"context"
"github.com/ocdp/cluster-service/internal/domain/entity"
)
// WorkspaceRepository Workspace 仓储接口
type WorkspaceRepository interface {
// Create 创建 Workspace
Create(ctx context.Context, workspace *entity.Workspace) error
// GetByID 根据 ID 获取 Workspace
GetByID(ctx context.Context, id string) (*entity.Workspace, error)
// GetByName 根据名称获取 Workspace
GetByName(ctx context.Context, name string) (*entity.Workspace, error)
// Update 更新 Workspace
Update(ctx context.Context, workspace *entity.Workspace) error
// Delete 删除 Workspace
Delete(ctx context.Context, id string) error
// List 列出所有 Workspace
List(ctx context.Context) ([]*entity.Workspace, error)
}

View File

@ -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)

View 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)
}

View File

@ -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
}

View 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)
}

View File

@ -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
}

View File

@ -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)

View 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
}

View File

@ -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)
}

View 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)
}

View 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 为 NULLadmin或空首个普通用户
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
}

View 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)
}

View 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)
}