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:
90
backend/internal/domain/entity/audit_log.go
Normal file
90
backend/internal/domain/entity/audit_log.go
Normal 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
|
||||
}
|
||||
41
backend/internal/domain/entity/chart_reference.go
Normal file
41
backend/internal/domain/entity/chart_reference.go
Normal 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
|
||||
}
|
||||
@ -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 // 所属 workspace,NULL 表示全局共享
|
||||
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 内容
|
||||
|
||||
@ -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")
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
79
backend/internal/domain/entity/quota.go
Normal file
79
backend/internal/domain/entity/quota.go
Normal 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
|
||||
}
|
||||
@ -7,24 +7,30 @@ import (
|
||||
// Registry OCI Registry 领域实体
|
||||
type Registry struct {
|
||||
ID string
|
||||
WorkspaceID string // 所属 workspace,NULL 表示全局共享
|
||||
OwnerID string // 创建者用户 ID
|
||||
Name string
|
||||
URL string
|
||||
Description string
|
||||
Username string
|
||||
Password string
|
||||
Insecure bool // 是否跳过 TLS 验证
|
||||
Insecure bool // 是否跳过 TLS 验证
|
||||
IsShared bool // 是否为共享 Registry(admin 创建供多 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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
98
backend/internal/domain/entity/storage.go
Normal file
98
backend/internal/domain/entity/storage.go
Normal 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
|
||||
}
|
||||
@ -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
|
||||
|
||||
83
backend/internal/domain/entity/values_template.go
Normal file
83
backend/internal/domain/entity/values_template.go
Normal 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,
|
||||
}
|
||||
}
|
||||
35
backend/internal/domain/entity/workspace.go
Normal file
35
backend/internal/domain/entity/workspace.go
Normal 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
|
||||
}
|
||||
27
backend/internal/domain/repository/audit_log_repository.go
Normal file
27
backend/internal/domain/repository/audit_log_repository.go
Normal 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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
30
backend/internal/domain/repository/quota_repository.go
Normal file
30
backend/internal/domain/repository/quota_repository.go
Normal 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
|
||||
}
|
||||
36
backend/internal/domain/repository/storage_repository.go
Normal file
36
backend/internal/domain/repository/storage_repository.go
Normal 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)
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
27
backend/internal/domain/repository/workspace_repository.go
Normal file
27
backend/internal/domain/repository/workspace_repository.go
Normal 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)
|
||||
}
|
||||
@ -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