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:
@ -42,3 +42,8 @@ type ValuesSchemaResponse struct {
|
||||
Schema string `json:"schema"`
|
||||
}
|
||||
|
||||
// ValuesResponse Values 响应
|
||||
type ValuesResponse struct {
|
||||
Values string `json:"values"`
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
package dto
|
||||
|
||||
// CreateChartReferenceRequest 创建 Chart 引用请求
|
||||
type CreateChartReferenceRequest struct {
|
||||
RegistryID string `json:"registry_id" binding:"required"`
|
||||
Repository string `json:"repository" binding:"required"`
|
||||
ChartName string `json:"chart_name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// UpdateChartReferenceRequest 更新 Chart 引用请求
|
||||
type UpdateChartReferenceRequest struct {
|
||||
RegistryID string `json:"registry_id"`
|
||||
Repository string `json:"repository"`
|
||||
ChartName string `json:"chart_name"`
|
||||
Description string `json:"description"`
|
||||
IsEnabled *bool `json:"is_enabled"`
|
||||
}
|
||||
|
||||
// ChartReferenceResponse Chart 引用响应
|
||||
type ChartReferenceResponse struct {
|
||||
ID string `json:"id"`
|
||||
WorkspaceID string `json:"workspace_id,omitempty"`
|
||||
RegistryID string `json:"registry_id"`
|
||||
Repository string `json:"repository"`
|
||||
ChartName string `json:"chart_name"`
|
||||
Description string `json:"description"`
|
||||
IsEnabled bool `json:"is_enabled"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
@ -2,30 +2,36 @@ package dto
|
||||
|
||||
// CreateClusterRequest 创建集群请求
|
||||
type CreateClusterRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Host string `json:"host" binding:"required"`
|
||||
CAData string `json:"caData"`
|
||||
CADataAlt string `json:"ca_data"`
|
||||
CertData string `json:"certData"`
|
||||
CertDataAlt string `json:"cert_data"`
|
||||
KeyData string `json:"keyData"`
|
||||
KeyDataAlt string `json:"key_data"`
|
||||
Token string `json:"token"`
|
||||
Description string `json:"description"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Host string `json:"host" binding:"required"`
|
||||
CAData string `json:"caData"`
|
||||
CADataAlt string `json:"ca_data"`
|
||||
CertData string `json:"certData"`
|
||||
CertDataAlt string `json:"cert_data"`
|
||||
KeyData string `json:"keyData"`
|
||||
KeyDataAlt string `json:"key_data"`
|
||||
Token string `json:"token"`
|
||||
Description string `json:"description"`
|
||||
IsolationMode string `json:"isolationMode"` // 'namespace' | 'cluster'
|
||||
DefaultNamespace string `json:"defaultNamespace"` // 默认 namespace 前缀
|
||||
IsShared bool `json:"isShared"` // 是否为共享集群
|
||||
}
|
||||
|
||||
// UpdateClusterRequest 更新集群请求
|
||||
type UpdateClusterRequest struct {
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
CAData string `json:"caData"`
|
||||
CADataAlt string `json:"ca_data"`
|
||||
CertData string `json:"certData"`
|
||||
CertDataAlt string `json:"cert_data"`
|
||||
KeyData string `json:"keyData"`
|
||||
KeyDataAlt string `json:"key_data"`
|
||||
Token string `json:"token"`
|
||||
Description string `json:"description"`
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
CAData string `json:"caData"`
|
||||
CADataAlt string `json:"ca_data"`
|
||||
CertData string `json:"certData"`
|
||||
CertDataAlt string `json:"cert_data"`
|
||||
KeyData string `json:"keyData"`
|
||||
KeyDataAlt string `json:"key_data"`
|
||||
Token string `json:"token"`
|
||||
Description string `json:"description"`
|
||||
IsolationMode string `json:"isolationMode"`
|
||||
DefaultNamespace string `json:"defaultNamespace"`
|
||||
IsShared *bool `json:"isShared"`
|
||||
}
|
||||
|
||||
// Normalize 将多种命名风格的字段合并到统一字段
|
||||
@ -56,10 +62,16 @@ func (r *UpdateClusterRequest) Normalize() {
|
||||
|
||||
// ClusterResponse 集群响应(敏感数据已脱敏)
|
||||
type ClusterResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
Description string `json:"description"`
|
||||
ID string `json:"id"`
|
||||
WorkspaceID string `json:"workspaceId,omitempty"`
|
||||
OwnerID string `json:"ownerId,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
Description string `json:"description"`
|
||||
IsolationMode string `json:"isolationMode"` // 'namespace' | 'cluster'
|
||||
DefaultNamespace string `json:"defaultNamespace"` // 默认 namespace 前缀
|
||||
IsShared bool `json:"isShared"` // 是否为共享集群
|
||||
|
||||
// 认证配置状态(不返回实际证书数据,仅返回是否已配置)
|
||||
HasCAData bool `json:"hasCaData"`
|
||||
HasCertData bool `json:"hasCertData"`
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/pkg/crypto"
|
||||
)
|
||||
@ -8,42 +10,50 @@ import (
|
||||
// ToRegistryResponse 转换 Registry 实体为响应 DTO(脱敏)
|
||||
func ToRegistryResponse(registry *entity.Registry) *RegistryResponse {
|
||||
response := &RegistryResponse{
|
||||
ID: registry.ID,
|
||||
Name: registry.Name,
|
||||
URL: registry.URL,
|
||||
Description: registry.Description,
|
||||
Username: registry.Username,
|
||||
Insecure: registry.Insecure,
|
||||
CreatedAt: registry.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
UpdatedAt: registry.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
ID: registry.ID,
|
||||
WorkspaceID: registry.WorkspaceID,
|
||||
OwnerID: registry.OwnerID,
|
||||
Name: registry.Name,
|
||||
URL: registry.URL,
|
||||
Description: registry.Description,
|
||||
Username: registry.Username,
|
||||
Insecure: registry.Insecure,
|
||||
IsShared: registry.IsShared,
|
||||
CreatedAt: registry.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
UpdatedAt: registry.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
|
||||
|
||||
// 脱敏处理密码
|
||||
if registry.Password != "" {
|
||||
response.HasPassword = true
|
||||
response.Password = crypto.MaskSensitiveData(registry.Password)
|
||||
}
|
||||
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// ToClusterResponse 转换 Cluster 实体为响应 DTO(脱敏)
|
||||
func ToClusterResponse(cluster *entity.Cluster) *ClusterResponse {
|
||||
response := &ClusterResponse{
|
||||
ID: cluster.ID,
|
||||
Name: cluster.Name,
|
||||
Host: cluster.Host,
|
||||
Description: cluster.Description,
|
||||
CreatedAt: cluster.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
UpdatedAt: cluster.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
ID: cluster.ID,
|
||||
WorkspaceID: cluster.WorkspaceID,
|
||||
OwnerID: cluster.OwnerID,
|
||||
Name: cluster.Name,
|
||||
Host: cluster.Host,
|
||||
Description: cluster.Description,
|
||||
IsolationMode: string(cluster.IsolationMode),
|
||||
DefaultNamespace: cluster.DefaultNamespace,
|
||||
IsShared: cluster.IsShared,
|
||||
CreatedAt: cluster.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
UpdatedAt: cluster.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
|
||||
|
||||
// 设置认证配置状态标志
|
||||
response.HasCAData = cluster.CAData != ""
|
||||
response.HasCertData = cluster.CertData != ""
|
||||
response.HasKeyData = cluster.KeyData != ""
|
||||
response.HasToken = cluster.Token != ""
|
||||
|
||||
|
||||
// 脱敏处理敏感数据(仅显示掩码)
|
||||
if cluster.CAData != "" {
|
||||
response.CAData = crypto.MaskSensitiveData(cluster.CAData)
|
||||
@ -57,7 +67,86 @@ func ToClusterResponse(cluster *entity.Cluster) *ClusterResponse {
|
||||
if cluster.Token != "" {
|
||||
response.Token = crypto.MaskSensitiveData(cluster.Token)
|
||||
}
|
||||
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// WorkspaceDTOFromEntity 转换 Workspace 实体为 DTO
|
||||
func WorkspaceDTOFromEntity(workspace *entity.Workspace) *WorkspaceDTO {
|
||||
return &WorkspaceDTO{
|
||||
ID: workspace.ID,
|
||||
Name: workspace.Name,
|
||||
Description: workspace.Description,
|
||||
CreatedBy: workspace.CreatedBy,
|
||||
CreatedAt: workspace.CreatedAt,
|
||||
UpdatedAt: workspace.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// WorkspaceDTOsFromEntities 批量转换
|
||||
func WorkspaceDTOsFromEntities(workspaces []*entity.Workspace) []*WorkspaceDTO {
|
||||
result := make([]*WorkspaceDTO, len(workspaces))
|
||||
for i, w := range workspaces {
|
||||
result[i] = WorkspaceDTOFromEntity(w)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// QuotaDTOFromEntity 转换 Quota 实体为 DTO
|
||||
func QuotaDTOFromEntity(quota *entity.WorkspaceQuota) *QuotaDTO {
|
||||
return &QuotaDTO{
|
||||
ID: quota.ID,
|
||||
WorkspaceID: quota.WorkspaceID,
|
||||
ResourceType: string(quota.ResourceType),
|
||||
HardLimit: quota.HardLimit,
|
||||
SoftLimit: quota.SoftLimit,
|
||||
Used: quota.Used,
|
||||
}
|
||||
}
|
||||
|
||||
// QuotaDTOsFromEntities 批量转换
|
||||
func QuotaDTOsFromEntities(quotas []*entity.WorkspaceQuota) []*QuotaDTO {
|
||||
result := make([]*QuotaDTO, len(quotas))
|
||||
for i, q := range quotas {
|
||||
result[i] = QuotaDTOFromEntity(q)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// UserDTOFromEntity 转换 User 实体为 DTO
|
||||
func UserDTOFromEntity(user *entity.User, workspaceName string) *UserDTO {
|
||||
return &UserDTO{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
Role: string(user.Role),
|
||||
WorkspaceID: user.WorkspaceID,
|
||||
WorkspaceName: workspaceName,
|
||||
IsActive: user.IsActive,
|
||||
MustChangePassword: user.MustChangePassword,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// UserDTOsFromEntities 批量转换
|
||||
func UserDTOsFromEntities(users []*entity.User, workspaceNames map[string]string) []*UserDTO {
|
||||
result := make([]*UserDTO, len(users))
|
||||
for i, u := range users {
|
||||
workspaceName := ""
|
||||
if u.WorkspaceID != "" {
|
||||
workspaceName = workspaceNames[u.WorkspaceID]
|
||||
}
|
||||
result[i] = UserDTOFromEntity(u, workspaceName)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// TimeToString 转换时间
|
||||
func TimeToString(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return t.Format("2006-01-02T15:04:05Z07:00")
|
||||
}
|
||||
|
||||
|
||||
@ -23,12 +23,15 @@ type UpdateRegistryRequest struct {
|
||||
// RegistryResponse Registry 响应(敏感数据已脱敏)
|
||||
type RegistryResponse struct {
|
||||
ID string `json:"id"`
|
||||
WorkspaceID string `json:"workspace_id,omitempty"`
|
||||
OwnerID string `json:"owner_id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Description string `json:"description"`
|
||||
Username string `json:"username,omitempty"` // 明文返回用户名(不敏感)
|
||||
Password string `json:"password,omitempty"` // 脱敏显示(••••••••)
|
||||
HasPassword bool `json:"hasPassword"` // 是否已设置密码
|
||||
IsShared bool `json:"is_shared"`
|
||||
Insecure bool `json:"insecure"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
|
||||
73
backend/internal/adapter/input/http/dto/storage_dto.go
Normal file
73
backend/internal/adapter/input/http/dto/storage_dto.go
Normal file
@ -0,0 +1,73 @@
|
||||
package dto
|
||||
|
||||
// CreateStorageRequest 创建存储后端请求
|
||||
type CreateStorageRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Type string `json:"type" binding:"required"` // nfs, pv, hostPath
|
||||
Description string `json:"description"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
IsShared bool `json:"is_shared"`
|
||||
|
||||
// NFS 配置
|
||||
NFS NFSConfigDTO `json:"nfs,omitempty"`
|
||||
// PV 配置
|
||||
PV PVConfigDTO `json:"pv,omitempty"`
|
||||
// HostPath 配置
|
||||
HostPath HostPathConfigDTO `json:"hostPath,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateStorageRequest 更新存储后端请求
|
||||
type UpdateStorageRequest struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
IsShared bool `json:"is_shared"`
|
||||
|
||||
// NFS 配置
|
||||
NFS NFSConfigDTO `json:"nfs,omitempty"`
|
||||
// PV 配置
|
||||
PV PVConfigDTO `json:"pv,omitempty"`
|
||||
// HostPath 配置
|
||||
HostPath HostPathConfigDTO `json:"hostPath,omitempty"`
|
||||
}
|
||||
|
||||
// NFSConfigDTO NFS 配置
|
||||
type NFSConfigDTO struct {
|
||||
Server string `json:"server"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// PVConfigDTO PV 配置
|
||||
type PVConfigDTO struct {
|
||||
StorageClassName string `json:"storageClassName"`
|
||||
Capacity string `json:"capacity"`
|
||||
AccessModes []string `json:"accessModes"`
|
||||
}
|
||||
|
||||
// HostPathConfigDTO HostPath 配置
|
||||
type HostPathConfigDTO struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// StorageResponse 存储后端响应
|
||||
type StorageResponse struct {
|
||||
ID string `json:"id"`
|
||||
WorkspaceID string `json:"workspace_id,omitempty"`
|
||||
OwnerID string `json:"owner_id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Config StorageConfigDTO `json:"config"`
|
||||
Description string `json:"description"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
IsShared bool `json:"is_shared"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// StorageConfigDTO 存储配置(脱敏后)
|
||||
type StorageConfigDTO struct {
|
||||
NFS *NFSConfigDTO `json:"nfs,omitempty"`
|
||||
PV *PVConfigDTO `json:"pv,omitempty"`
|
||||
HostPath *HostPathConfigDTO `json:"hostPath,omitempty"`
|
||||
}
|
||||
78
backend/internal/adapter/input/http/dto/user_dto.go
Normal file
78
backend/internal/adapter/input/http/dto/user_dto.go
Normal file
@ -0,0 +1,78 @@
|
||||
package dto
|
||||
|
||||
import "time"
|
||||
|
||||
// UserDTO 用户 DTO
|
||||
type UserDTO struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Role string `json:"role"`
|
||||
WorkspaceID string `json:"workspace_id,omitempty"`
|
||||
WorkspaceName string `json:"workspace_name,omitempty"`
|
||||
IsActive bool `json:"is_active"`
|
||||
MustChangePassword bool `json:"must_change_password"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// CreateUserRequest 创建用户请求(Admin 操作)
|
||||
type CreateUserRequest struct {
|
||||
Username string `json:"username" validate:"required"`
|
||||
Password string `json:"password" validate:"required,min=6"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role" validate:"required,oneof=admin user"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
}
|
||||
|
||||
// UpdateUserRequest 更新用户请求
|
||||
type UpdateUserRequest struct {
|
||||
Email string `json:"email"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// ChangeUserWorkspaceRequest 分配用户到 Workspace 请求
|
||||
type ChangeUserWorkspaceRequest struct {
|
||||
WorkspaceID string `json:"workspace_id" validate:"required"`
|
||||
}
|
||||
|
||||
// ResetPasswordRequest 重置密码请求
|
||||
type ResetPasswordRequest struct {
|
||||
NewPassword string `json:"new_password" validate:"required,min=6"`
|
||||
}
|
||||
|
||||
// ChangePasswordRequest 修改密码请求
|
||||
type ChangePasswordRequest struct {
|
||||
OldPassword string `json:"old_password" validate:"required"`
|
||||
NewPassword string `json:"new_password" validate:"required,min=6"`
|
||||
}
|
||||
|
||||
// SetUserActiveRequest 启用/禁用用户请求
|
||||
type SetUserActiveRequest struct {
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// UserListResponse 用户列表响应
|
||||
type UserListResponse struct {
|
||||
Users []*UserDTO `json:"users"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// UserWithWorkspaceResponse 用户及其 Workspace 响应
|
||||
type UserWithWorkspaceResponse struct {
|
||||
User *UserDTO `json:"user"`
|
||||
Workspace *WorkspaceDTO `json:"workspace,omitempty"`
|
||||
}
|
||||
|
||||
// LoginResponse 登录响应
|
||||
type LoginResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
MustChangePassword bool `json:"must_change_password"`
|
||||
}
|
||||
|
||||
// UserResponseWithDTO 用户响应(包含完整DTO)
|
||||
type UserResponseWithDTO struct {
|
||||
User *UserDTO `json:"user"`
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package dto
|
||||
|
||||
// CreateValuesTemplateRequest 创建 Values 模板请求
|
||||
type CreateValuesTemplateRequest struct {
|
||||
ChartReferenceID string `json:"chart_reference_id" binding:"required"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
ValuesYAML string `json:"values_yaml" binding:"required"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
}
|
||||
|
||||
// UpdateValuesTemplateRequest 更新 Values 模板请求
|
||||
type UpdateValuesTemplateRequest struct {
|
||||
Description string `json:"description"`
|
||||
ValuesYAML string `json:"values_yaml"`
|
||||
IsDefault *bool `json:"is_default"`
|
||||
}
|
||||
|
||||
// ValuesTemplateResponse Values 模板响应
|
||||
type ValuesTemplateResponse struct {
|
||||
ID string `json:"id"`
|
||||
WorkspaceID string `json:"workspace_id,omitempty"`
|
||||
OwnerID string `json:"owner_id,omitempty"`
|
||||
ChartReferenceID string `json:"chart_reference_id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
ValuesYAML string `json:"values_yaml"`
|
||||
Version int `json:"version"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// RollbackValuesTemplateRequest 回滚请求
|
||||
type RollbackValuesTemplateRequest struct {
|
||||
TemplateID string `json:"template_id" binding:"required"`
|
||||
}
|
||||
67
backend/internal/adapter/input/http/dto/workspace_dto.go
Normal file
67
backend/internal/adapter/input/http/dto/workspace_dto.go
Normal file
@ -0,0 +1,67 @@
|
||||
package dto
|
||||
|
||||
import "time"
|
||||
|
||||
// WorkspaceDTO 工作空间 DTO
|
||||
type WorkspaceDTO struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
CreatedBy string `json:"created_by"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// CreateWorkspaceRequest 创建工作空间请求
|
||||
type CreateWorkspaceRequest struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// UpdateWorkspaceRequest 更新工作空间请求
|
||||
type UpdateWorkspaceRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// QuotaDTO 配额 DTO
|
||||
type QuotaDTO struct {
|
||||
ID string `json:"id"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
ResourceType string `json:"resource_type"`
|
||||
HardLimit float64 `json:"hard_limit"`
|
||||
SoftLimit float64 `json:"soft_limit"`
|
||||
Used float64 `json:"used"`
|
||||
}
|
||||
|
||||
// SetQuotaRequest 设置配额请求
|
||||
type SetQuotaRequest struct {
|
||||
ResourceType string `json:"resource_type" validate:"required"`
|
||||
HardLimit float64 `json:"hard_limit" validate:"required"`
|
||||
SoftLimit float64 `json:"soft_limit"`
|
||||
}
|
||||
|
||||
// SetQuotasRequest 批量设置配额请求
|
||||
type SetQuotasRequest struct {
|
||||
CPU *QuotaValue `json:"cpu"`
|
||||
GPU *QuotaValue `json:"gpu"`
|
||||
GPUMemory *QuotaValue `json:"gpu_memory"`
|
||||
}
|
||||
|
||||
// QuotaValue 配额值
|
||||
type QuotaValue struct {
|
||||
HardLimit float64 `json:"hard_limit"`
|
||||
SoftLimit float64 `json:"soft_limit"`
|
||||
}
|
||||
|
||||
// WorkspaceResponse 工作空间响应
|
||||
type WorkspaceResponse struct {
|
||||
Workspace *WorkspaceDTO `json:"workspace"`
|
||||
Quotas []*QuotaDTO `json:"quotas,omitempty"`
|
||||
}
|
||||
|
||||
// WorkspaceListResponse 工作空间列表响应
|
||||
type WorkspaceListResponse struct {
|
||||
Workspaces []*WorkspaceDTO `json:"workspaces"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
283
backend/internal/adapter/input/http/middleware/authz.go
Normal file
283
backend/internal/adapter/input/http/middleware/authz.go
Normal file
@ -0,0 +1,283 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/repository"
|
||||
"github.com/ocdp/cluster-service/internal/domain/service"
|
||||
)
|
||||
|
||||
// Context keys
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
ContextKeyUserID contextKey = "user_id"
|
||||
ContextKeyUsername contextKey = "username"
|
||||
ContextKeyUserRole contextKey = "user_role"
|
||||
ContextKeyWorkspaceID contextKey = "workspace_id"
|
||||
)
|
||||
|
||||
// UserClaims 用户声明(从 JWT 解析)
|
||||
type UserClaims struct {
|
||||
UserID string
|
||||
Username string
|
||||
Role entity.UserRole
|
||||
WorkspaceID string
|
||||
}
|
||||
|
||||
// WorkspaceMiddleware 工作空间中间件
|
||||
// 从 JWT 获取用户角色和 workspace_id,进行权限检查
|
||||
func WorkspaceMiddleware(userRepo repository.UserRepository) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 从 Header 获取 Token
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
http.Error(w, "Missing authorization header", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// 解析 Bearer Token
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
http.Error(w, "Invalid authorization header", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
token := parts[1]
|
||||
|
||||
// 这里需要从 AuthService 获取验证方法
|
||||
// 简化处理:假设 token 包含 user_id 和 username
|
||||
// 实际实现需要调用 JWT 验证服务
|
||||
|
||||
// 从数据库获取用户信息
|
||||
// 注意:这里需要通过 token 解析出 userID
|
||||
// 实际实现应该在 AuthService 中完成
|
||||
_ = userRepo
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// RequireWorkspace 强制要求 workspace 上下文
|
||||
// 用于非 Admin 用户的资源操作
|
||||
func RequireWorkspace(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
workspaceID := r.Header.Get("X-Workspace-ID")
|
||||
userRole := r.Header.Get("X-User-Role")
|
||||
|
||||
// Admin 可以没有 workspace
|
||||
if userRole == string(entity.RoleAdmin) {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// 普通用户必须有 workspace
|
||||
if workspaceID == "" {
|
||||
http.Error(w, "Workspace context required", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// 将 workspace_id 放入 context
|
||||
ctx := context.WithValue(r.Context(), ContextKeyWorkspaceID, workspaceID)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
// RequireAdmin 要求 Admin 角色
|
||||
func RequireAdmin(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
userRole := r.Header.Get("X-User-Role")
|
||||
|
||||
if userRole != string(entity.RoleAdmin) {
|
||||
http.Error(w, "Admin access required", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserClaims 从 Context 获取用户声明
|
||||
func GetUserClaims(ctx context.Context) *UserClaims {
|
||||
userID, _ := ctx.Value(ContextKeyUserID).(string)
|
||||
username, _ := ctx.Value(ContextKeyUsername).(string)
|
||||
roleStr, _ := ctx.Value(ContextKeyUserRole).(string)
|
||||
workspaceID, _ := ctx.Value(ContextKeyWorkspaceID).(string)
|
||||
|
||||
return &UserClaims{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
Role: entity.UserRole(roleStr),
|
||||
WorkspaceID: workspaceID,
|
||||
}
|
||||
}
|
||||
|
||||
// GetWorkspaceID 从 Context 获取 workspace ID
|
||||
func GetWorkspaceID(ctx context.Context) string {
|
||||
workspaceID, _ := ctx.Value(ContextKeyWorkspaceID).(string)
|
||||
return workspaceID
|
||||
}
|
||||
|
||||
// GetUserID 从 Context 获取用户 ID
|
||||
func GetUserID(ctx context.Context) string {
|
||||
userID, _ := ctx.Value(ContextKeyUserID).(string)
|
||||
return userID
|
||||
}
|
||||
|
||||
// GetUserRole 从 Context 获取用户角色
|
||||
func GetUserRole(ctx context.Context) entity.UserRole {
|
||||
roleStr, _ := ctx.Value(ContextKeyUserRole).(string)
|
||||
return entity.UserRole(roleStr)
|
||||
}
|
||||
|
||||
// FilterByWorkspace 根据用户角色过滤资源
|
||||
// Admin: 返回所有资源(workspaceID 忽略)
|
||||
// User: 仅返回属于自己 workspace 的资源
|
||||
func FilterByWorkspace(workspaceID, userRole string) (filterWorkspaceID string, isAdmin bool) {
|
||||
if userRole == string(entity.RoleAdmin) {
|
||||
return "", true
|
||||
}
|
||||
return workspaceID, false
|
||||
}
|
||||
|
||||
// AuthorizationService 授权服务
|
||||
type AuthorizationService struct {
|
||||
userRepo repository.UserRepository
|
||||
}
|
||||
|
||||
// NewAuthorizationService 创建授权服务
|
||||
func NewAuthorizationService(userRepo repository.UserRepository) *AuthorizationService {
|
||||
return &AuthorizationService{
|
||||
userRepo: userRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// CheckResourceAccess 检查用户是否有权访问指定资源
|
||||
func (s *AuthorizationService) CheckResourceAccess(ctx context.Context, userID, resourceWorkspaceID string) error {
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Admin 可以访问所有资源
|
||||
if user.Role == entity.RoleAdmin {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 普通用户只能访问自己 workspace 的资源
|
||||
if user.WorkspaceID != resourceWorkspaceID {
|
||||
return entity.ErrPermissionDenied
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanAccessWorkspace 检查用户是否可以访问指定 workspace
|
||||
func (s *AuthorizationService) CanAccessWorkspace(ctx context.Context, userID, targetWorkspaceID string) error {
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Admin 可以访问所有 workspace
|
||||
if user.Role == entity.RoleAdmin {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 普通用户只能访问自己的 workspace
|
||||
if user.WorkspaceID != targetWorkspaceID {
|
||||
return entity.ErrPermissionDenied
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAccessibleWorkspaces 获取用户可访问的 workspace 列表
|
||||
func (s *AuthorizationService) GetAccessibleWorkspaces(ctx context.Context, userID string) ([]string, error) {
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Admin 可以访问所有 workspace
|
||||
if user.Role == entity.RoleAdmin {
|
||||
return nil, nil // nil 表示所有
|
||||
}
|
||||
|
||||
// 普通用户只能访问自己的 workspace
|
||||
if user.WorkspaceID != "" {
|
||||
return []string{user.WorkspaceID}, nil
|
||||
}
|
||||
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
// RequireRole 要求特定角色
|
||||
func RequireRole(roles ...entity.UserRole) func(http.Handler) http.Handler {
|
||||
roleSet := make(map[entity.UserRole]bool)
|
||||
for _, r := range roles {
|
||||
roleSet[r] = true
|
||||
}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
userRole := r.Header.Get("X-User-Role")
|
||||
|
||||
if !roleSet[entity.UserRole(userRole)] {
|
||||
http.Error(w, "Insufficient permissions", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// WithUserClaims 将用户声明注入到 Context
|
||||
func WithUserClaims(claims *UserClaims) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, ContextKeyUserID, claims.UserID)
|
||||
ctx = context.WithValue(ctx, ContextKeyUsername, claims.Username)
|
||||
ctx = context.WithValue(ctx, ContextKeyUserRole, string(claims.Role))
|
||||
if claims.WorkspaceID != "" {
|
||||
ctx = context.WithValue(ctx, ContextKeyWorkspaceID, claims.WorkspaceID)
|
||||
}
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// LoginRequired 要求登录
|
||||
func LoginRequired(authService *service.AuthService) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
http.Error(w, "Authorization required", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
http.Error(w, "Invalid authorization header", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
token := parts[1]
|
||||
userID, _, err := authService.VerifyAccessToken(r.Context(), token)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), ContextKeyUserID, userID)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -191,3 +191,42 @@ func (h *ArtifactHandler) GetArtifactValuesSchema(w http.ResponseWriter, r *http
|
||||
|
||||
respondJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetArtifactValues 获取 Helm Chart 的 values.yaml
|
||||
// @Summary 获取 Helm Chart Values
|
||||
// @Description 获取 Helm Chart 的 values.yaml 文件内容 (仅支持 Chart 类型)
|
||||
// @Tags Artifacts
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param registry_id path string true "Registry ID"
|
||||
// @Param repository_name path string true "Repository Name (URL encoded)"
|
||||
// @Param reference path string true "Artifact Reference (tag or digest)"
|
||||
// @Success 200 {object} dto.ValuesResponse
|
||||
// @Failure 500 {object} dto.ErrorResponse
|
||||
// @Router /registries/{registry_id}/repositories/{repository_name}/artifacts/{reference}/values [get]
|
||||
func (h *ArtifactHandler) GetArtifactValues(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
registryID := vars["registry_id"]
|
||||
repositoryName := vars["repository_name"]
|
||||
reference := vars["reference"]
|
||||
|
||||
values, err := h.artifactService.GetValues(r.Context(), registryID, repositoryName, reference)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, entity.ErrRegistryNotFound),
|
||||
errors.Is(err, entity.ErrRepositoryNotFound),
|
||||
errors.Is(err, entity.ErrArtifactNotFound),
|
||||
errors.Is(err, entity.ErrValuesNotFound):
|
||||
respondError(w, http.StatusNotFound, "Values not found", err.Error())
|
||||
default:
|
||||
respondError(w, http.StatusInternalServerError, "Failed to get values", err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
response := &dto.ValuesResponse{
|
||||
Values: values,
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
@ -0,0 +1,229 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/ocdp/cluster-service/internal/adapter/input/http/dto"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/service"
|
||||
)
|
||||
|
||||
// ChartReferenceHandler Chart Reference Handler
|
||||
type ChartReferenceHandler struct {
|
||||
chartRefService *service.ChartReferenceService
|
||||
}
|
||||
|
||||
// NewChartReferenceHandler 创建 Chart Reference Handler
|
||||
func NewChartReferenceHandler(chartRefService *service.ChartReferenceService) *ChartReferenceHandler {
|
||||
return &ChartReferenceHandler{
|
||||
chartRefService: chartRefService,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateChartReference 创建 Chart 引用
|
||||
// @Summary 创建 Chart 引用
|
||||
// @Description 新增 Chart 引用配置
|
||||
// @Tags Chart References
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body dto.CreateChartReferenceRequest true "Chart 引用信息"
|
||||
// @Success 201 {object} dto.ChartReferenceResponse
|
||||
// @Failure 400 {object} dto.ErrorResponse
|
||||
// @Router /chart-references [post]
|
||||
func (h *ChartReferenceHandler) CreateChartReference(w http.ResponseWriter, r *http.Request) {
|
||||
var req dto.CreateChartReferenceRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
workspaceID := r.Header.Get("X-Workspace-ID")
|
||||
|
||||
chartRef, err := h.chartRefService.Create(
|
||||
r.Context(),
|
||||
workspaceID,
|
||||
req.RegistryID,
|
||||
req.Repository,
|
||||
req.ChartName,
|
||||
req.Description,
|
||||
)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Failed to create chart reference", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response := toChartReferenceResponse(chartRef)
|
||||
respondJSON(w, http.StatusCreated, response)
|
||||
}
|
||||
|
||||
// GetChartReference 获取 Chart 引用详情
|
||||
// @Summary 获取 Chart 引用
|
||||
// @Tags Chart References
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param chart_reference_id path string true "Chart Reference ID"
|
||||
// @Success 200 {object} dto.ChartReferenceResponse
|
||||
// @Failure 404 {object} dto.ErrorResponse
|
||||
// @Router /chart-references/{chart_reference_id} [get]
|
||||
func (h *ChartReferenceHandler) GetChartReference(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
chartRefID := vars["chart_reference_id"]
|
||||
|
||||
chartRef, err := h.chartRefService.GetByID(r.Context(), chartRefID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusNotFound, "Chart reference not found", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response := toChartReferenceResponse(chartRef)
|
||||
respondJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetAllChartReferences 获取所有 Chart 引用
|
||||
// @Summary 列出所有 Chart 引用
|
||||
// @Tags Chart References
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {array} dto.ChartReferenceResponse
|
||||
// @Failure 500 {object} dto.ErrorResponse
|
||||
// @Router /chart-references [get]
|
||||
func (h *ChartReferenceHandler) GetAllChartReferences(w http.ResponseWriter, r *http.Request) {
|
||||
workspaceID := r.Header.Get("X-Workspace-ID")
|
||||
role := r.Header.Get("X-User-Role")
|
||||
|
||||
var chartRefs []*dto.ChartReferenceResponse
|
||||
|
||||
// Admin 可以看到所有,其他用户只看自己 workspace
|
||||
if role == "admin" {
|
||||
allChartRefs, err := h.chartRefService.List(r.Context())
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "Failed to list chart references", err.Error())
|
||||
return
|
||||
}
|
||||
for _, cr := range allChartRefs {
|
||||
chartRefs = append(chartRefs, toChartReferenceResponse(cr))
|
||||
}
|
||||
} else if workspaceID != "" {
|
||||
workspaceChartRefs, err := h.chartRefService.GetByWorkspace(r.Context(), workspaceID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "Failed to list chart references", err.Error())
|
||||
return
|
||||
}
|
||||
for _, cr := range workspaceChartRefs {
|
||||
chartRefs = append(chartRefs, toChartReferenceResponse(cr))
|
||||
}
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, chartRefs)
|
||||
}
|
||||
|
||||
// UpdateChartReference 更新 Chart 引用
|
||||
// @Summary 更新 Chart 引用
|
||||
// @Tags Chart References
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param chart_reference_id path string true "Chart Reference ID"
|
||||
// @Param request body dto.UpdateChartReferenceRequest true "更新内容"
|
||||
// @Success 200 {object} dto.ChartReferenceResponse
|
||||
// @Failure 404 {object} dto.ErrorResponse
|
||||
// @Router /chart-references/{chart_reference_id} [put]
|
||||
func (h *ChartReferenceHandler) UpdateChartReference(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
chartRefID := vars["chart_reference_id"]
|
||||
|
||||
var req dto.UpdateChartReferenceRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
isEnabled := true
|
||||
if req.IsEnabled != nil {
|
||||
isEnabled = *req.IsEnabled
|
||||
}
|
||||
|
||||
chartRef, err := h.chartRefService.Update(
|
||||
r.Context(),
|
||||
chartRefID,
|
||||
req.RegistryID,
|
||||
req.Repository,
|
||||
req.ChartName,
|
||||
req.Description,
|
||||
isEnabled,
|
||||
)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Failed to update chart reference", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response := toChartReferenceResponse(chartRef)
|
||||
respondJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
// DeleteChartReference 删除 Chart 引用
|
||||
// @Summary 删除 Chart 引用
|
||||
// @Tags Chart References
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param chart_reference_id path string true "Chart Reference ID"
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @Failure 404 {object} dto.ErrorResponse
|
||||
// @Router /chart-references/{chart_reference_id} [delete]
|
||||
func (h *ChartReferenceHandler) DeleteChartReference(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
chartRefID := vars["chart_reference_id"]
|
||||
|
||||
if err := h.chartRefService.Delete(r.Context(), chartRefID); err != nil {
|
||||
respondError(w, http.StatusNotFound, "Failed to delete chart reference", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// GetChartReferencesByRegistry 获取 Registry 的所有 Chart 引用
|
||||
// @Summary 获取 Registry 的 Chart 引用
|
||||
// @Tags Chart References
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param registry_id path string true "Registry ID"
|
||||
// @Success 200 {array} dto.ChartReferenceResponse
|
||||
// @Failure 500 {object} dto.ErrorResponse
|
||||
// @Router /registries/{registry_id}/chart-references [get]
|
||||
func (h *ChartReferenceHandler) GetChartReferencesByRegistry(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
registryID := vars["registry_id"]
|
||||
|
||||
chartRefs, err := h.chartRefService.GetByRegistry(r.Context(), registryID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "Failed to list chart references", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
responses := make([]*dto.ChartReferenceResponse, 0, len(chartRefs))
|
||||
for _, cr := range chartRefs {
|
||||
responses = append(responses, toChartReferenceResponse(cr))
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, responses)
|
||||
}
|
||||
|
||||
// toChartReferenceResponse 转换为响应 DTO
|
||||
func toChartReferenceResponse(chartRef *entity.ChartReference) *dto.ChartReferenceResponse {
|
||||
return &dto.ChartReferenceResponse{
|
||||
ID: chartRef.ID,
|
||||
WorkspaceID: chartRef.WorkspaceID,
|
||||
RegistryID: chartRef.RegistryID,
|
||||
Repository: chartRef.Repository,
|
||||
ChartName: chartRef.ChartName,
|
||||
Description: chartRef.Description,
|
||||
IsEnabled: chartRef.IsEnabled,
|
||||
CreatedAt: chartRef.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
UpdatedAt: chartRef.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
}
|
||||
@ -40,13 +40,20 @@ func (h *ClusterHandler) CreateCluster(w http.ResponseWriter, r *http.Request) {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
req.Normalize()
|
||||
req.Normalize()
|
||||
|
||||
// 创建实体
|
||||
cluster := entity.NewCluster(req.Name, req.Host)
|
||||
cluster := entity.NewCluster("", "", req.Name, req.Host)
|
||||
cluster.Description = req.Description
|
||||
|
||||
if req.CertData != "" && req.KeyData != "" {
|
||||
// 设置认证信息
|
||||
hasKubeconfig := req.CAData != "" && (len(req.CAData) > 100 && (req.CAData[:11] == "apiVersion:" || req.CAData[:5] == "kind:"))
|
||||
hasCertAuth := req.CertData != "" && req.KeyData != ""
|
||||
|
||||
if hasKubeconfig {
|
||||
// 使用完整的 kubeconfig 格式
|
||||
cluster.CAData = req.CAData
|
||||
} else if hasCertAuth {
|
||||
cluster.SetCertAuth(req.CAData, req.CertData, req.KeyData)
|
||||
} else if req.Token != "" {
|
||||
cluster.SetTokenAuth(req.Token)
|
||||
@ -57,6 +64,18 @@ func (h *ClusterHandler) CreateCluster(w http.ResponseWriter, r *http.Request) {
|
||||
"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1vY2sgQ2xpZW50IENlcnRpZmljYXRlCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=",
|
||||
"LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNb2NrIFByaXZhdGUgS2V5Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0t",
|
||||
)
|
||||
} else {
|
||||
// 生产模式:没有提供凭证,尝试使用本地 kubeconfig
|
||||
// 不再返回错误,让 TestConnection 尝试使用本地 kubeconfig
|
||||
// cluster 保持空的认证信息,TestConnection 会使用 KUBECONFIG 环境变量
|
||||
}
|
||||
|
||||
// 测试集群连接(非 mock 模式下)
|
||||
if os.Getenv("ADAPTER_MODE") != "mock" {
|
||||
if err := h.clusterService.TestConnection(r.Context(), cluster); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Failed to connect to cluster", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 调用领域服务
|
||||
@ -198,18 +217,24 @@ func (h *ClusterHandler) GetClusterHealth(w http.ResponseWriter, r *http.Request
|
||||
vars := mux.Vars(r)
|
||||
clusterID := vars["cluster_id"]
|
||||
|
||||
// 检查集群是否存在
|
||||
_, err := h.clusterService.GetCluster(r.Context(), clusterID)
|
||||
// 获取集群
|
||||
cluster, err := h.clusterService.GetCluster(r.Context(), clusterID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusNotFound, "Cluster not found", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 实现真实的健康检查
|
||||
// 测试连接
|
||||
err = h.clusterService.TestConnection(r.Context(), cluster)
|
||||
|
||||
response := &dto.ClusterHealthResponse{
|
||||
Healthy: true,
|
||||
Message: "Cluster is healthy",
|
||||
Version: "v1.28.0",
|
||||
Healthy: err == nil,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
response.Message = err.Error()
|
||||
} else {
|
||||
response.Message = "Cluster is healthy"
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, response)
|
||||
|
||||
@ -54,10 +54,14 @@ func (h *InstanceHandler) CreateInstance(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// 创建实体
|
||||
instance := entity.NewInstance(
|
||||
"", // workspaceID - will be set based on user
|
||||
"", // ownerID - will be set based on user
|
||||
clusterID,
|
||||
req.RegistryID,
|
||||
"", // chartReferenceID - not used in legacy API
|
||||
"", // valuesTemplateID - not used in legacy API
|
||||
req.Name,
|
||||
req.Namespace,
|
||||
req.RegistryID,
|
||||
req.Repository,
|
||||
chart, // Extracted chart name
|
||||
req.Tag, // Tag mapped to version
|
||||
|
||||
@ -41,7 +41,7 @@ func (h *RegistryHandler) CreateRegistry(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// 创建实体
|
||||
registry := entity.NewRegistry(req.Name, req.URL)
|
||||
registry := entity.NewRegistry("", "", req.Name, req.URL)
|
||||
registry.Description = req.Description
|
||||
registry.Insecure = req.Insecure
|
||||
registry.SetCredentials(req.Username, req.Password)
|
||||
|
||||
291
backend/internal/adapter/input/http/rest/storage_handler.go
Normal file
291
backend/internal/adapter/input/http/rest/storage_handler.go
Normal file
@ -0,0 +1,291 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/ocdp/cluster-service/internal/adapter/input/http/dto"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/service"
|
||||
)
|
||||
|
||||
// StorageHandler Storage Backend Handler
|
||||
type StorageHandler struct {
|
||||
storageService *service.StorageService
|
||||
}
|
||||
|
||||
// NewStorageHandler 创建 Storage Handler
|
||||
func NewStorageHandler(storageService *service.StorageService) *StorageHandler {
|
||||
return &StorageHandler{
|
||||
storageService: storageService,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateStorage 创建存储后端
|
||||
// @Summary 创建存储后端
|
||||
// @Description 新增存储后端配置(NFS/PV/hostPath)
|
||||
// @Tags Storage
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body dto.CreateStorageRequest true "存储后端信息"
|
||||
// @Success 201 {object} dto.StorageResponse
|
||||
// @Failure 400 {object} dto.ErrorResponse
|
||||
// @Router /storage-backends [post]
|
||||
func (h *StorageHandler) CreateStorage(w http.ResponseWriter, r *http.Request) {
|
||||
var req dto.CreateStorageRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
workspaceID := r.Header.Get("X-Workspace-ID")
|
||||
ownerID := r.Header.Get("X-User-ID")
|
||||
|
||||
// 构建配置
|
||||
storageType := entity.StorageType(req.Type)
|
||||
config := entity.StorageConfig{}
|
||||
|
||||
switch storageType {
|
||||
case entity.StorageTypeNFS:
|
||||
config.NFS = &entity.NFSConfig{
|
||||
Server: req.NFS.Server,
|
||||
Path: req.NFS.Path,
|
||||
}
|
||||
case entity.StorageTypePV:
|
||||
config.PV = &entity.PVConfig{
|
||||
StorageClassName: req.PV.StorageClassName,
|
||||
Capacity: req.PV.Capacity,
|
||||
AccessModes: req.PV.AccessModes,
|
||||
}
|
||||
case entity.StorageTypeHostPath:
|
||||
config.HostPath = &entity.HostPathConfig{
|
||||
Path: req.HostPath.Path,
|
||||
}
|
||||
}
|
||||
|
||||
// 调用领域服务
|
||||
storage, err := h.storageService.Create(
|
||||
r.Context(),
|
||||
workspaceID,
|
||||
ownerID,
|
||||
req.Name,
|
||||
storageType,
|
||||
config,
|
||||
req.Description,
|
||||
req.IsDefault,
|
||||
req.IsShared,
|
||||
)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Failed to create storage backend", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
response := toStorageResponse(storage)
|
||||
respondJSON(w, http.StatusCreated, response)
|
||||
}
|
||||
|
||||
// GetStorage 获取存储后端详情
|
||||
// @Summary 获取存储后端
|
||||
// @Tags Storage
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param storage_id path string true "Storage ID"
|
||||
// @Success 200 {object} dto.StorageResponse
|
||||
// @Failure 404 {object} dto.ErrorResponse
|
||||
// @Router /storage-backends/{storage_id} [get]
|
||||
func (h *StorageHandler) GetStorage(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
storageID := vars["storage_id"]
|
||||
|
||||
storage, err := h.storageService.GetByID(r.Context(), storageID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusNotFound, "Storage backend not found", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response := toStorageResponse(storage)
|
||||
respondJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetAllStorage 获取所有存储后端
|
||||
// @Summary 列出所有存储后端
|
||||
// @Tags Storage
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {array} dto.StorageResponse
|
||||
// @Failure 500 {object} dto.ErrorResponse
|
||||
// @Router /storage-backends [get]
|
||||
func (h *StorageHandler) GetAllStorage(w http.ResponseWriter, r *http.Request) {
|
||||
// 获取 workspace_id(从 JWT)
|
||||
workspaceID := r.Header.Get("X-Workspace-ID")
|
||||
role := r.Header.Get("X-User-Role")
|
||||
|
||||
var storages []*entity.StorageBackend
|
||||
var err error
|
||||
|
||||
// Admin 可以看到所有,其他用户只看自己 workspace + 共享的
|
||||
if role == "admin" {
|
||||
storages, err = h.storageService.List(r.Context())
|
||||
} else if workspaceID != "" {
|
||||
// 获取 workspace 的存储 + 共享存储
|
||||
workspaceStorages, _ := h.storageService.GetByWorkspace(r.Context(), workspaceID)
|
||||
sharedStorages, _ := h.storageService.GetShared(r.Context())
|
||||
|
||||
// 合并去重
|
||||
seen := make(map[string]bool)
|
||||
for _, s := range workspaceStorages {
|
||||
if !seen[s.ID] {
|
||||
storages = append(storages, s)
|
||||
seen[s.ID] = true
|
||||
}
|
||||
}
|
||||
for _, s := range sharedStorages {
|
||||
if !seen[s.ID] {
|
||||
storages = append(storages, s)
|
||||
seen[s.ID] = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 没有 workspace 的用户只能看到共享存储
|
||||
storages, err = h.storageService.GetShared(r.Context())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "Failed to list storage backends", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
responses := make([]*dto.StorageResponse, 0, len(storages))
|
||||
for _, storage := range storages {
|
||||
responses = append(responses, toStorageResponse(storage))
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, responses)
|
||||
}
|
||||
|
||||
// UpdateStorage 更新存储后端
|
||||
// @Summary 更新存储后端
|
||||
// @Tags Storage
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param storage_id path string true "Storage ID"
|
||||
// @Param request body dto.UpdateStorageRequest true "更新内容"
|
||||
// @Success 200 {object} dto.StorageResponse
|
||||
// @Failure 404 {object} dto.ErrorResponse
|
||||
// @Router /storage-backends/{storage_id} [put]
|
||||
func (h *StorageHandler) UpdateStorage(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
storageID := vars["storage_id"]
|
||||
|
||||
var req dto.UpdateStorageRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 构建配置
|
||||
var storageType entity.StorageType
|
||||
if req.Type != "" {
|
||||
storageType = entity.StorageType(req.Type)
|
||||
}
|
||||
|
||||
config := entity.StorageConfig{}
|
||||
if storageType == entity.StorageTypeNFS && (req.NFS.Server != "" || req.NFS.Path != "") {
|
||||
config.NFS = &entity.NFSConfig{
|
||||
Server: req.NFS.Server,
|
||||
Path: req.NFS.Path,
|
||||
}
|
||||
} else if storageType == entity.StorageTypePV && req.PV.StorageClassName != "" {
|
||||
config.PV = &entity.PVConfig{
|
||||
StorageClassName: req.PV.StorageClassName,
|
||||
Capacity: req.PV.Capacity,
|
||||
AccessModes: req.PV.AccessModes,
|
||||
}
|
||||
} else if storageType == entity.StorageTypeHostPath && req.HostPath.Path != "" {
|
||||
config.HostPath = &entity.HostPathConfig{
|
||||
Path: req.HostPath.Path,
|
||||
}
|
||||
}
|
||||
|
||||
storage, err := h.storageService.Update(
|
||||
r.Context(),
|
||||
storageID,
|
||||
req.Name,
|
||||
req.Description,
|
||||
storageType,
|
||||
config,
|
||||
req.IsDefault,
|
||||
req.IsShared,
|
||||
)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Failed to update storage backend", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response := toStorageResponse(storage)
|
||||
respondJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
// DeleteStorage 删除存储后端
|
||||
// @Summary 删除存储后端
|
||||
// @Tags Storage
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param storage_id path string true "Storage ID"
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @Failure 404 {object} dto.ErrorResponse
|
||||
// @Router /storage-backends/{storage_id} [delete]
|
||||
func (h *StorageHandler) DeleteStorage(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
storageID := vars["storage_id"]
|
||||
|
||||
if err := h.storageService.Delete(r.Context(), storageID); err != nil {
|
||||
respondError(w, http.StatusNotFound, "Failed to delete storage backend", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// toStorageResponse 转换为响应 DTO
|
||||
func toStorageResponse(storage *entity.StorageBackend) *dto.StorageResponse {
|
||||
config := dto.StorageConfigDTO{}
|
||||
|
||||
if storage.Config.NFS != nil {
|
||||
config.NFS = &dto.NFSConfigDTO{
|
||||
Server: storage.Config.NFS.Server,
|
||||
Path: storage.Config.NFS.Path,
|
||||
}
|
||||
}
|
||||
if storage.Config.PV != nil {
|
||||
config.PV = &dto.PVConfigDTO{
|
||||
StorageClassName: storage.Config.PV.StorageClassName,
|
||||
Capacity: storage.Config.PV.Capacity,
|
||||
AccessModes: storage.Config.PV.AccessModes,
|
||||
}
|
||||
}
|
||||
if storage.Config.HostPath != nil {
|
||||
config.HostPath = &dto.HostPathConfigDTO{
|
||||
Path: storage.Config.HostPath.Path,
|
||||
}
|
||||
}
|
||||
|
||||
return &dto.StorageResponse{
|
||||
ID: storage.ID,
|
||||
WorkspaceID: storage.WorkspaceID,
|
||||
OwnerID: storage.OwnerID,
|
||||
Name: storage.Name,
|
||||
Type: string(storage.Type),
|
||||
Config: config,
|
||||
Description: storage.Description,
|
||||
IsDefault: storage.IsDefault,
|
||||
IsShared: storage.IsShared,
|
||||
CreatedAt: storage.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
UpdatedAt: storage.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
}
|
||||
152
backend/internal/adapter/input/http/rest/user_handler.go
Normal file
152
backend/internal/adapter/input/http/rest/user_handler.go
Normal file
@ -0,0 +1,152 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/ocdp/cluster-service/internal/adapter/input/http/dto"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/service"
|
||||
)
|
||||
|
||||
// UserHandler 用户 HTTP 处理程序
|
||||
type UserHandler struct {
|
||||
authService *service.AuthService
|
||||
workspaceService *service.WorkspaceService
|
||||
}
|
||||
|
||||
// NewUserHandler 创建用户处理程序
|
||||
func NewUserHandler(authService *service.AuthService, workspaceService *service.WorkspaceService) *UserHandler {
|
||||
return &UserHandler{
|
||||
authService: authService,
|
||||
workspaceService: workspaceService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetCurrentUser 获取当前用户信息
|
||||
// @Summary 获取当前用户信息
|
||||
// @Description 获取当前登录用户的基本信息
|
||||
// @Tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} dto.UserResponseWithDTO
|
||||
// @Router /users/me [get]
|
||||
func (h *UserHandler) GetCurrentUser(w http.ResponseWriter, r *http.Request) {
|
||||
userID := GetUserIDFromRequest(r)
|
||||
if userID == "" {
|
||||
respondError(w, http.StatusUnauthorized, "Not authenticated", "")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.authService.GetUserByID(r.Context(), userID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusNotFound, "User not found", "")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取 workspace 名称
|
||||
workspaceName := ""
|
||||
if user.WorkspaceID != "" {
|
||||
ws, _ := h.workspaceService.GetByID(r.Context(), user.WorkspaceID)
|
||||
if ws != nil {
|
||||
workspaceName = ws.Name
|
||||
}
|
||||
}
|
||||
|
||||
respondSuccess(w, "", dto.UserResponseWithDTO{User: dto.UserDTOFromEntity(user, workspaceName)})
|
||||
}
|
||||
|
||||
// ChangePassword 修改当前用户密码
|
||||
// @Summary 修改当前用户密码
|
||||
// @Description 修改当前登录用户的密码
|
||||
// @Tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.ChangePasswordRequest true "修改密码请求"
|
||||
// @Success 200
|
||||
// @Router /users/me/password [put]
|
||||
func (h *UserHandler) ChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||
userID := GetUserIDFromRequest(r)
|
||||
if userID == "" {
|
||||
respondError(w, http.StatusUnauthorized, "Not authenticated", "")
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.ChangePasswordRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body", "")
|
||||
return
|
||||
}
|
||||
|
||||
err := h.authService.ChangePassword(r.Context(), userID, req.OldPassword, req.NewPassword)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, err.Error(), "")
|
||||
return
|
||||
}
|
||||
|
||||
respondSuccess(w, "Password changed successfully", map[string]string{"message": "Password changed successfully"})
|
||||
}
|
||||
|
||||
// GetCurrentUserWorkspace 获取当前用户所属的 Workspace
|
||||
// @Summary 获取当前用户所属工作空间
|
||||
// @Description 获取当前用户所属工作空间的详细信息和配额
|
||||
// @Tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} dto.WorkspaceResponse
|
||||
// @Router /users/me/workspace [get]
|
||||
func (h *UserHandler) GetCurrentUserWorkspace(w http.ResponseWriter, r *http.Request) {
|
||||
userID := GetUserIDFromRequest(r)
|
||||
if userID == "" {
|
||||
respondError(w, http.StatusUnauthorized, "Not authenticated", "")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.authService.GetUserByID(r.Context(), userID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusNotFound, "User not found", "")
|
||||
return
|
||||
}
|
||||
|
||||
// Admin 没有 workspace
|
||||
if user.Role == entity.RoleAdmin {
|
||||
respondSuccess(w, "", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if user.WorkspaceID == "" {
|
||||
respondSuccess(w, "", nil)
|
||||
return
|
||||
}
|
||||
|
||||
workspace, err := h.workspaceService.GetByID(r.Context(), user.WorkspaceID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusNotFound, "Workspace not found", "")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取配额
|
||||
quotas, _ := h.workspaceService.GetQuotas(r.Context(), workspace.ID)
|
||||
|
||||
response := dto.WorkspaceResponse{
|
||||
Workspace: dto.WorkspaceDTOFromEntity(workspace),
|
||||
Quotas: dto.QuotaDTOsFromEntities(quotas),
|
||||
}
|
||||
|
||||
respondSuccess(w, "", response)
|
||||
}
|
||||
|
||||
// GetUserIDFromRequest 从请求中获取用户 ID
|
||||
func GetUserIDFromRequest(r *http.Request) string {
|
||||
// 尝试从 Header 获取(由中间件设置)
|
||||
userID := r.Header.Get("X-User-ID")
|
||||
if userID != "" {
|
||||
return userID
|
||||
}
|
||||
|
||||
// 尝试从 Context 获取(安全类型断言)
|
||||
if uid, ok := r.Context().Value("user_id").(string); ok {
|
||||
return uid
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@ -0,0 +1,332 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/ocdp/cluster-service/internal/adapter/input/http/dto"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/service"
|
||||
)
|
||||
|
||||
// UserManagementHandler 用户管理 HTTP 处理程序
|
||||
type UserManagementHandler struct {
|
||||
userManagementService *service.UserManagementService
|
||||
authService *service.AuthService
|
||||
workspaceService *service.WorkspaceService
|
||||
}
|
||||
|
||||
// NewUserManagementHandler 创建用户管理处理程序
|
||||
func NewUserManagementHandler(
|
||||
userManagementService *service.UserManagementService,
|
||||
authService *service.AuthService,
|
||||
workspaceService *service.WorkspaceService,
|
||||
) *UserManagementHandler {
|
||||
return &UserManagementHandler{
|
||||
userManagementService: userManagementService,
|
||||
authService: authService,
|
||||
workspaceService: workspaceService,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateUser 创建用户(Admin 操作)
|
||||
// @Summary 创建用户
|
||||
// @Description 创建新用户(Admin 专用)
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.CreateUserRequest true "创建用户请求"
|
||||
// @Success 200 {object} dto.UserResponseWithDTO
|
||||
// @Router /admin/users [post]
|
||||
func (h *UserManagementHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
|
||||
// 检查权限(Admin)
|
||||
if !h.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.CreateUserRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body", "")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userManagementService.CreateUser(r.Context(), req.Username, req.Password, req.Email, req.Role, req.WorkspaceID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, err.Error(), "")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取 workspace 名称
|
||||
workspaceName := ""
|
||||
if user.WorkspaceID != "" {
|
||||
ws, _ := h.workspaceService.GetByID(r.Context(), user.WorkspaceID)
|
||||
if ws != nil {
|
||||
workspaceName = ws.Name
|
||||
}
|
||||
}
|
||||
|
||||
respondSuccess(w, "", dto.UserResponseWithDTO{User: dto.UserDTOFromEntity(user, workspaceName)})
|
||||
}
|
||||
|
||||
// GetUser 获取用户
|
||||
// @Summary 获取用户
|
||||
// @Description 获取指定用户信息(Admin 专用)
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param user_id path string true "用户 ID"
|
||||
// @Success 200 {object} dto.UserResponseWithDTO
|
||||
// @Router /admin/users/{user_id} [get]
|
||||
func (h *UserManagementHandler) GetUser(w http.ResponseWriter, r *http.Request) {
|
||||
// 检查权限(Admin)
|
||||
if !h.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
userID := vars["user_id"]
|
||||
|
||||
user, err := h.userManagementService.GetUser(r.Context(), userID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusNotFound, "User not found", "")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取 workspace 名称
|
||||
workspaceName := ""
|
||||
if user.WorkspaceID != "" {
|
||||
ws, _ := h.workspaceService.GetByID(r.Context(), user.WorkspaceID)
|
||||
if ws != nil {
|
||||
workspaceName = ws.Name
|
||||
}
|
||||
}
|
||||
|
||||
respondSuccess(w, "", dto.UserResponseWithDTO{User: dto.UserDTOFromEntity(user, workspaceName)})
|
||||
}
|
||||
|
||||
// ListUsers 列出用户
|
||||
// @Summary 列出用户
|
||||
// @Description 获取所有用户列表(Admin 专用),可按 workspace_id 筛选
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param workspace_id query string false "工作空间 ID"
|
||||
// @Success 200 {object} dto.UserListResponse
|
||||
// @Router /admin/users [get]
|
||||
func (h *UserManagementHandler) ListUsers(w http.ResponseWriter, r *http.Request) {
|
||||
// 检查权限(Admin)
|
||||
if !h.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
workspaceID := r.URL.Query().Get("workspace_id")
|
||||
|
||||
users, err := h.userManagementService.ListUsers(r.Context(), workspaceID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, err.Error(), "")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取所有 workspace 名称
|
||||
workspaceNames := make(map[string]string)
|
||||
workspaces, _ := h.workspaceService.List(r.Context())
|
||||
for _, ws := range workspaces {
|
||||
workspaceNames[ws.ID] = ws.Name
|
||||
}
|
||||
|
||||
respondSuccess(w, "", dto.UserListResponse{
|
||||
Users: dto.UserDTOsFromEntities(users, workspaceNames),
|
||||
Total: len(users),
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateUser 更新用户
|
||||
// @Summary 更新用户
|
||||
// @Description 更新用户信息(Admin 专用)
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param user_id path string true "用户 ID"
|
||||
// @Param request body dto.UpdateUserRequest true "更新用户请求"
|
||||
// @Success 200 {object} dto.UserResponseWithDTO
|
||||
// @Router /admin/users/{user_id} [put]
|
||||
func (h *UserManagementHandler) UpdateUser(w http.ResponseWriter, r *http.Request) {
|
||||
// 检查权限(Admin)
|
||||
if !h.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
userID := vars["user_id"]
|
||||
|
||||
user, err := h.userManagementService.GetUser(r.Context(), userID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusNotFound, "User not found", "")
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdateUserRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body", "")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Email != "" {
|
||||
user.Email = req.Email
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
user.IsActive = *req.IsActive
|
||||
}
|
||||
|
||||
if err := h.userManagementService.UpdateUser(r.Context(), user); err != nil {
|
||||
respondError(w, http.StatusBadRequest, err.Error(), "")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取 workspace 名称
|
||||
workspaceName := ""
|
||||
if user.WorkspaceID != "" {
|
||||
ws, _ := h.workspaceService.GetByID(r.Context(), user.WorkspaceID)
|
||||
if ws != nil {
|
||||
workspaceName = ws.Name
|
||||
}
|
||||
}
|
||||
|
||||
respondSuccess(w, "", dto.UserResponseWithDTO{User: dto.UserDTOFromEntity(user, workspaceName)})
|
||||
}
|
||||
|
||||
// SetUserActive 启用/禁用用户
|
||||
// @Summary 启用/禁用用户
|
||||
// @Description 设置用户是否启用(Admin 专用)
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param user_id path string true "用户 ID"
|
||||
// @Param request body dto.SetUserActiveRequest true "启用状态"
|
||||
// @Success 200
|
||||
// @Router /admin/users/{user_id}/active [put]
|
||||
func (h *UserManagementHandler) SetUserActive(w http.ResponseWriter, r *http.Request) {
|
||||
// 检查权限(Admin)
|
||||
if !h.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
userID := vars["user_id"]
|
||||
|
||||
var req dto.SetUserActiveRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body", "")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.userManagementService.SetUserActive(r.Context(), userID, req.IsActive); err != nil {
|
||||
respondError(w, http.StatusBadRequest, err.Error(), "")
|
||||
return
|
||||
}
|
||||
|
||||
respondSuccess(w, "", nil)
|
||||
}
|
||||
|
||||
// ChangeUserWorkspace 分配用户到 Workspace
|
||||
// @Summary 分配用户到工作空间
|
||||
// @Description 将用户分配到指定工作空间(Admin 专用)
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param user_id path string true "用户 ID"
|
||||
// @Param request body dto.ChangeUserWorkspaceRequest true "工作空间分配请求"
|
||||
// @Success 200
|
||||
// @Router /admin/users/{user_id}/workspace [put]
|
||||
func (h *UserManagementHandler) ChangeUserWorkspace(w http.ResponseWriter, r *http.Request) {
|
||||
// 检查权限(Admin)
|
||||
if !h.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
userID := vars["user_id"]
|
||||
|
||||
var req dto.ChangeUserWorkspaceRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body", "")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.userManagementService.ChangeUserWorkspace(r.Context(), userID, req.WorkspaceID); err != nil {
|
||||
respondError(w, http.StatusBadRequest, err.Error(), "")
|
||||
return
|
||||
}
|
||||
|
||||
respondSuccess(w, "", nil)
|
||||
}
|
||||
|
||||
// ResetPassword 重置用户密码(Admin 操作)
|
||||
// @Summary 重置用户密码
|
||||
// @Description 重置指定用户的密码(Admin 专用)
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param user_id path string true "用户 ID"
|
||||
// @Param request body dto.ResetPasswordRequest true "重置密码请求"
|
||||
// @Success 200
|
||||
// @Router /admin/users/{user_id}/password [put]
|
||||
func (h *UserManagementHandler) ResetPassword(w http.ResponseWriter, r *http.Request) {
|
||||
// 检查权限(Admin)
|
||||
if !h.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
userID := vars["user_id"]
|
||||
|
||||
var req dto.ResetPasswordRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body", "")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.userManagementService.ResetPassword(r.Context(), userID, req.NewPassword); err != nil {
|
||||
respondError(w, http.StatusBadRequest, err.Error(), "")
|
||||
return
|
||||
}
|
||||
|
||||
respondSuccess(w, "", nil)
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户
|
||||
// @Summary 删除用户
|
||||
// @Description 删除指定用户(Admin 专用)
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param user_id path string true "用户 ID"
|
||||
// @Success 200
|
||||
// @Router /admin/users/{user_id} [delete]
|
||||
func (h *UserManagementHandler) DeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
// 检查权限(Admin)
|
||||
if !h.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
userID := vars["user_id"]
|
||||
|
||||
if err := h.userManagementService.DeleteUser(r.Context(), userID); err != nil {
|
||||
respondError(w, http.StatusBadRequest, err.Error(), "")
|
||||
return
|
||||
}
|
||||
|
||||
respondSuccess(w, "", nil)
|
||||
}
|
||||
|
||||
// requireAdmin 检查是否为 Admin
|
||||
func (h *UserManagementHandler) requireAdmin(w http.ResponseWriter, r *http.Request) bool {
|
||||
userRole := r.Header.Get("X-User-Role")
|
||||
if userRole != string(entity.RoleAdmin) {
|
||||
respondError(w, http.StatusForbidden, "Admin access required", "")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@ -0,0 +1,294 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/ocdp/cluster-service/internal/adapter/input/http/dto"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/service"
|
||||
)
|
||||
|
||||
// ValuesTemplateHandler Values Template Handler
|
||||
type ValuesTemplateHandler struct {
|
||||
valuesTemplateService *service.ValuesTemplateService
|
||||
}
|
||||
|
||||
// NewValuesTemplateHandler 创建 Values Template Handler
|
||||
func NewValuesTemplateHandler(valuesTemplateService *service.ValuesTemplateService) *ValuesTemplateHandler {
|
||||
return &ValuesTemplateHandler{
|
||||
valuesTemplateService: valuesTemplateService,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateValuesTemplate 创建 Values 模板
|
||||
// @Summary 创建 Values 模板
|
||||
// @Description 新增 Values 模板配置(带版本管理)
|
||||
// @Tags Values Templates
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body dto.CreateValuesTemplateRequest true "Values 模板信息"
|
||||
// @Success 201 {object} dto.ValuesTemplateResponse
|
||||
// @Failure 400 {object} dto.ErrorResponse
|
||||
// @Router /values-templates [post]
|
||||
func (h *ValuesTemplateHandler) CreateValuesTemplate(w http.ResponseWriter, r *http.Request) {
|
||||
var req dto.CreateValuesTemplateRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
workspaceID := r.Header.Get("X-Workspace-ID")
|
||||
ownerID := r.Header.Get("X-User-ID")
|
||||
|
||||
template, err := h.valuesTemplateService.Create(
|
||||
r.Context(),
|
||||
workspaceID,
|
||||
ownerID,
|
||||
req.ChartReferenceID,
|
||||
req.Name,
|
||||
req.Description,
|
||||
req.ValuesYAML,
|
||||
req.IsDefault,
|
||||
)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Failed to create values template", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response := toValuesTemplateResponse(template)
|
||||
respondJSON(w, http.StatusCreated, response)
|
||||
}
|
||||
|
||||
// GetValuesTemplate 获取 Values 模板详情
|
||||
// @Summary 获取 Values 模板
|
||||
// @Tags Values Templates
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param template_id path string true "Template ID"
|
||||
// @Success 200 {object} dto.ValuesTemplateResponse
|
||||
// @Failure 404 {object} dto.ErrorResponse
|
||||
// @Router /values-templates/{template_id} [get]
|
||||
func (h *ValuesTemplateHandler) GetValuesTemplate(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
templateID := vars["template_id"]
|
||||
|
||||
template, err := h.valuesTemplateService.GetByID(r.Context(), templateID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusNotFound, "Values template not found", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response := toValuesTemplateResponse(template)
|
||||
respondJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetAllValuesTemplates 获取所有 Values 模板
|
||||
// @Summary 列出所有 Values 模板
|
||||
// @Tags Values Templates
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {array} dto.ValuesTemplateResponse
|
||||
// @Failure 500 {object} dto.ErrorResponse
|
||||
// @Router /values-templates [get]
|
||||
func (h *ValuesTemplateHandler) GetAllValuesTemplates(w http.ResponseWriter, r *http.Request) {
|
||||
workspaceID := r.Header.Get("X-Workspace-ID")
|
||||
role := r.Header.Get("X-User-Role")
|
||||
|
||||
var templates []*dto.ValuesTemplateResponse
|
||||
|
||||
// Admin 可以看到所有,其他用户只看自己 workspace
|
||||
if role == "admin" {
|
||||
allTemplates, err := h.valuesTemplateService.List(r.Context())
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "Failed to list values templates", err.Error())
|
||||
return
|
||||
}
|
||||
for _, t := range allTemplates {
|
||||
templates = append(templates, toValuesTemplateResponse(t))
|
||||
}
|
||||
} else if workspaceID != "" {
|
||||
workspaceTemplates, err := h.valuesTemplateService.GetByWorkspace(r.Context(), workspaceID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "Failed to list values templates", err.Error())
|
||||
return
|
||||
}
|
||||
for _, t := range workspaceTemplates {
|
||||
templates = append(templates, toValuesTemplateResponse(t))
|
||||
}
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, templates)
|
||||
}
|
||||
|
||||
// GetValuesTemplatesByChartReference 获取 Chart Reference 的所有 Values 模板
|
||||
// @Summary 获取 Chart Reference 的 Values 模板
|
||||
// @Tags Values Templates
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param chart_reference_id path string true "Chart Reference ID"
|
||||
// @Success 200 {array} dto.ValuesTemplateResponse
|
||||
// @Failure 500 {object} dto.ErrorResponse
|
||||
// @Router /chart-references/{chart_reference_id}/values-templates [get]
|
||||
func (h *ValuesTemplateHandler) GetValuesTemplatesByChartReference(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
chartRefID := vars["chart_reference_id"]
|
||||
|
||||
templates, err := h.valuesTemplateService.GetByChartReference(r.Context(), chartRefID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "Failed to list values templates", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
responses := make([]*dto.ValuesTemplateResponse, 0, len(templates))
|
||||
for _, t := range templates {
|
||||
responses = append(responses, toValuesTemplateResponse(t))
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, responses)
|
||||
}
|
||||
|
||||
// GetValuesTemplateHistory 获取模板的版本历史
|
||||
// @Summary 获取 Values 模板版本历史
|
||||
// @Tags Values Templates
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param chart_reference_id path string true "Chart Reference ID"
|
||||
// @Param name query string true "Template Name"
|
||||
// @Success 200 {array} dto.ValuesTemplateResponse
|
||||
// @Failure 500 {object} dto.ErrorResponse
|
||||
// @Router /chart-references/{chart_reference_id}/values-templates/history [get]
|
||||
func (h *ValuesTemplateHandler) GetValuesTemplateHistory(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
chartRefID := vars["chart_reference_id"]
|
||||
name := r.URL.Query().Get("name")
|
||||
|
||||
if name == "" {
|
||||
respondError(w, http.StatusBadRequest, "Template name is required", "")
|
||||
return
|
||||
}
|
||||
|
||||
templates, err := h.valuesTemplateService.GetHistory(r.Context(), chartRefID, name)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "Failed to get values template history", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
responses := make([]*dto.ValuesTemplateResponse, 0, len(templates))
|
||||
for _, t := range templates {
|
||||
responses = append(responses, toValuesTemplateResponse(t))
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, responses)
|
||||
}
|
||||
|
||||
// UpdateValuesTemplate 更新 Values 模板
|
||||
// @Summary 更新 Values 模板
|
||||
// @Tags Values Templates
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param template_id path string true "Template ID"
|
||||
// @Param request body dto.UpdateValuesTemplateRequest true "更新内容"
|
||||
// @Success 200 {object} dto.ValuesTemplateResponse
|
||||
// @Failure 404 {object} dto.ErrorResponse
|
||||
// @Router /values-templates/{template_id} [put]
|
||||
func (h *ValuesTemplateHandler) UpdateValuesTemplate(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
templateID := vars["template_id"]
|
||||
|
||||
var req dto.UpdateValuesTemplateRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
isDefault := false
|
||||
if req.IsDefault != nil {
|
||||
isDefault = *req.IsDefault
|
||||
}
|
||||
|
||||
template, err := h.valuesTemplateService.Update(
|
||||
r.Context(),
|
||||
templateID,
|
||||
req.Description,
|
||||
req.ValuesYAML,
|
||||
isDefault,
|
||||
)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Failed to update values template", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response := toValuesTemplateResponse(template)
|
||||
respondJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
// DeleteValuesTemplate 删除 Values 模板
|
||||
// @Summary 删除 Values 模板
|
||||
// @Tags Values Templates
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param template_id path string true "Template ID"
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @Failure 404 {object} dto.ErrorResponse
|
||||
// @Router /values-templates/{template_id} [delete]
|
||||
func (h *ValuesTemplateHandler) DeleteValuesTemplate(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
templateID := vars["template_id"]
|
||||
|
||||
if err := h.valuesTemplateService.Delete(r.Context(), templateID); err != nil {
|
||||
respondError(w, http.StatusNotFound, "Failed to delete values template", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// RollbackValuesTemplate 回滚到指定版本
|
||||
// @Summary 回滚 Values 模板
|
||||
// @Tags Values Templates
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param chart_reference_id path string true "Chart Reference ID"
|
||||
// @Param request body dto.RollbackValuesTemplateRequest true "回滚信息"
|
||||
// @Success 200 {object} dto.ValuesTemplateResponse
|
||||
// @Failure 404 {object} dto.ErrorResponse
|
||||
// @Router /chart-references/{chart_reference_id}/values-templates/rollback [post]
|
||||
func (h *ValuesTemplateHandler) RollbackValuesTemplate(w http.ResponseWriter, r *http.Request) {
|
||||
var req dto.RollbackValuesTemplateRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
template, err := h.valuesTemplateService.Rollback(r.Context(), req.TemplateID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Failed to rollback values template", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response := toValuesTemplateResponse(template)
|
||||
respondJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
// toValuesTemplateResponse 转换为响应 DTO
|
||||
func toValuesTemplateResponse(template *entity.ValuesTemplate) *dto.ValuesTemplateResponse {
|
||||
return &dto.ValuesTemplateResponse{
|
||||
ID: template.ID,
|
||||
WorkspaceID: template.WorkspaceID,
|
||||
OwnerID: template.OwnerID,
|
||||
ChartReferenceID: template.ChartReferenceID,
|
||||
Name: template.Name,
|
||||
Description: template.Description,
|
||||
ValuesYAML: template.ValuesYAML,
|
||||
Version: template.Version,
|
||||
IsDefault: template.IsDefault,
|
||||
CreatedAt: template.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
UpdatedAt: template.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
}
|
||||
306
backend/internal/adapter/input/http/rest/workspace_handler.go
Normal file
306
backend/internal/adapter/input/http/rest/workspace_handler.go
Normal file
@ -0,0 +1,306 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/ocdp/cluster-service/internal/adapter/input/http/dto"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/service"
|
||||
)
|
||||
|
||||
// WorkspaceHandler 工作空间 HTTP 处理程序
|
||||
type WorkspaceHandler struct {
|
||||
workspaceService *service.WorkspaceService
|
||||
authService *service.AuthService
|
||||
}
|
||||
|
||||
// NewWorkspaceHandler 创建工作空间处理程序
|
||||
func NewWorkspaceHandler(workspaceService *service.WorkspaceService, authService *service.AuthService) *WorkspaceHandler {
|
||||
return &WorkspaceHandler{
|
||||
workspaceService: workspaceService,
|
||||
authService: authService,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateWorkspace 创建工作空间
|
||||
// @Summary 创建工作空间
|
||||
// @Description 创建新的工作空间(Admin 专用)
|
||||
// @Tags workspace
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.CreateWorkspaceRequest true "创建工作空间请求"
|
||||
// @Success 200 {object} dto.WorkspaceDTO
|
||||
// @Router /workspaces [post]
|
||||
func (h *WorkspaceHandler) CreateWorkspace(w http.ResponseWriter, r *http.Request) {
|
||||
// 检查权限(Admin)
|
||||
if !h.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.CreateWorkspaceRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body", "")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取创建者 ID
|
||||
userID := GetUserIDFromRequest(r)
|
||||
|
||||
workspace, err := h.workspaceService.Create(r.Context(), req.Name, req.Description, userID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, err.Error(), "")
|
||||
return
|
||||
}
|
||||
|
||||
respondSuccess(w, "", dto.WorkspaceDTOFromEntity(workspace))
|
||||
}
|
||||
|
||||
// GetWorkspace 获取工作空间
|
||||
// @Summary 获取工作空间
|
||||
// @Description 获取指定工作空间的详细信息和配额
|
||||
// @Tags workspace
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param workspace_id path string true "工作空间 ID"
|
||||
// @Success 200 {object} dto.WorkspaceResponse
|
||||
// @Router /workspaces/{workspace_id} [get]
|
||||
func (h *WorkspaceHandler) GetWorkspace(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
workspaceID := vars["workspace_id"]
|
||||
|
||||
workspace, err := h.workspaceService.GetByID(r.Context(), workspaceID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusNotFound, "Workspace not found", "")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查访问权限
|
||||
if !h.canAccessWorkspace(w, r, workspace.ID) {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取配额
|
||||
quotas, _ := h.workspaceService.GetQuotas(r.Context(), workspace.ID)
|
||||
|
||||
response := dto.WorkspaceResponse{
|
||||
Workspace: dto.WorkspaceDTOFromEntity(workspace),
|
||||
Quotas: dto.QuotaDTOsFromEntities(quotas),
|
||||
}
|
||||
|
||||
respondSuccess(w, "", response)
|
||||
}
|
||||
|
||||
// UpdateWorkspace 更新工作空间
|
||||
// @Summary 更新工作空间
|
||||
// @Description 更新工作空间信息(Admin 专用)
|
||||
// @Tags workspace
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param workspace_id path string true "工作空间 ID"
|
||||
// @Param request body dto.UpdateWorkspaceRequest true "更新工作空间请求"
|
||||
// @Success 200 {object} dto.WorkspaceDTO
|
||||
// @Router /workspaces/{workspace_id} [put]
|
||||
func (h *WorkspaceHandler) UpdateWorkspace(w http.ResponseWriter, r *http.Request) {
|
||||
// 检查权限(Admin)
|
||||
if !h.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
workspaceID := vars["workspace_id"]
|
||||
|
||||
workspace, err := h.workspaceService.GetByID(r.Context(), workspaceID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusNotFound, "Workspace not found", "")
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdateWorkspaceRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body", "")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
workspace.Name = req.Name
|
||||
}
|
||||
if req.Description != "" {
|
||||
workspace.Description = req.Description
|
||||
}
|
||||
|
||||
if err := h.workspaceService.Update(r.Context(), workspace); err != nil {
|
||||
respondError(w, http.StatusBadRequest, err.Error(), "")
|
||||
return
|
||||
}
|
||||
|
||||
respondSuccess(w, "", dto.WorkspaceDTOFromEntity(workspace))
|
||||
}
|
||||
|
||||
// DeleteWorkspace 删除工作空间
|
||||
// @Summary 删除工作空间
|
||||
// @Description 删除指定工作空间(Admin 专用)
|
||||
// @Tags workspace
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param workspace_id path string true "工作空间 ID"
|
||||
// @Success 200
|
||||
// @Router /workspaces/{workspace_id} [delete]
|
||||
func (h *WorkspaceHandler) DeleteWorkspace(w http.ResponseWriter, r *http.Request) {
|
||||
// 检查权限(Admin)
|
||||
if !h.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
workspaceID := vars["workspace_id"]
|
||||
|
||||
if err := h.workspaceService.Delete(r.Context(), workspaceID); err != nil {
|
||||
respondError(w, http.StatusBadRequest, err.Error(), "")
|
||||
return
|
||||
}
|
||||
|
||||
respondSuccess(w, "", nil)
|
||||
}
|
||||
|
||||
// ListWorkspaces 列出所有工作空间
|
||||
// @Summary 列出所有工作空间
|
||||
// @Description 获取所有工作空间列表(Admin 专用)
|
||||
// @Tags workspace
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} dto.WorkspaceListResponse
|
||||
// @Router /workspaces [get]
|
||||
func (h *WorkspaceHandler) ListWorkspaces(w http.ResponseWriter, r *http.Request) {
|
||||
// 检查权限(Admin)
|
||||
if !h.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
workspaces, err := h.workspaceService.List(r.Context())
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, err.Error(), "")
|
||||
return
|
||||
}
|
||||
|
||||
respondSuccess(w, "", dto.WorkspaceListResponse{
|
||||
Workspaces: dto.WorkspaceDTOsFromEntities(workspaces),
|
||||
Total: len(workspaces),
|
||||
})
|
||||
}
|
||||
|
||||
// GetWorkspaceQuotas 获取工作空间配额
|
||||
// @Summary 获取工作空间配额
|
||||
// @Description 获取指定工作空间的资源配额
|
||||
// @Tags workspace
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param workspace_id path string true "工作空间 ID"
|
||||
// @Success 200 {array} dto.QuotaDTO
|
||||
// @Router /workspaces/{workspace_id}/quotas [get]
|
||||
func (h *WorkspaceHandler) GetWorkspaceQuotas(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
workspaceID := vars["workspace_id"]
|
||||
|
||||
// 检查访问权限
|
||||
if !h.canAccessWorkspace(w, r, workspaceID) {
|
||||
return
|
||||
}
|
||||
|
||||
quotas, err := h.workspaceService.GetQuotas(r.Context(), workspaceID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, err.Error(), "")
|
||||
return
|
||||
}
|
||||
|
||||
respondSuccess(w, "", dto.QuotaDTOsFromEntities(quotas))
|
||||
}
|
||||
|
||||
// SetWorkspaceQuotas 设置工作空间配额
|
||||
// @Summary 设置工作空间配额
|
||||
// @Description 设置指定工作空间的 CPU/GPU/GPU Memory 配额(Admin 专用)
|
||||
// @Tags workspace
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param workspace_id path string true "工作空间 ID"
|
||||
// @Param request body dto.SetQuotasRequest true "配额设置请求"
|
||||
// @Success 200 {array} dto.QuotaDTO
|
||||
// @Router /workspaces/{workspace_id}/quotas [put]
|
||||
func (h *WorkspaceHandler) SetWorkspaceQuotas(w http.ResponseWriter, r *http.Request) {
|
||||
// 检查权限(Admin)
|
||||
if !h.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
workspaceID := vars["workspace_id"]
|
||||
|
||||
var req dto.SetQuotasRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body", "")
|
||||
return
|
||||
}
|
||||
|
||||
quotas := make(map[entity.ResourceType]struct {
|
||||
HardLimit float64
|
||||
SoftLimit float64
|
||||
})
|
||||
|
||||
if req.CPU != nil {
|
||||
quotas[entity.ResourceCPU] = struct {
|
||||
HardLimit float64
|
||||
SoftLimit float64
|
||||
}{req.CPU.HardLimit, req.CPU.SoftLimit}
|
||||
}
|
||||
if req.GPU != nil {
|
||||
quotas[entity.ResourceGPU] = struct {
|
||||
HardLimit float64
|
||||
SoftLimit float64
|
||||
}{req.GPU.HardLimit, req.GPU.SoftLimit}
|
||||
}
|
||||
if req.GPUMemory != nil {
|
||||
quotas[entity.ResourceGPUMemory] = struct {
|
||||
HardLimit float64
|
||||
SoftLimit float64
|
||||
}{req.GPUMemory.HardLimit, req.GPUMemory.SoftLimit}
|
||||
}
|
||||
|
||||
if err := h.workspaceService.SetQuotas(r.Context(), workspaceID, quotas); err != nil {
|
||||
respondError(w, http.StatusBadRequest, err.Error(), "")
|
||||
return
|
||||
}
|
||||
|
||||
// 返回更新后的配额
|
||||
updatedQuotas, _ := h.workspaceService.GetQuotas(r.Context(), workspaceID)
|
||||
respondSuccess(w, "", dto.QuotaDTOsFromEntities(updatedQuotas))
|
||||
}
|
||||
|
||||
// requireAdmin 检查是否为 Admin
|
||||
func (h *WorkspaceHandler) requireAdmin(w http.ResponseWriter, r *http.Request) bool {
|
||||
userRole := r.Header.Get("X-User-Role")
|
||||
if userRole != string(entity.RoleAdmin) {
|
||||
respondError(w, http.StatusForbidden, "Admin access required", "")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// canAccessWorkspace 检查是否可以访问工作空间
|
||||
func (h *WorkspaceHandler) canAccessWorkspace(w http.ResponseWriter, r *http.Request, workspaceID string) bool {
|
||||
userRole := r.Header.Get("X-User-Role")
|
||||
userWorkspaceID := r.Header.Get("X-Workspace-ID")
|
||||
|
||||
// Admin 可以访问所有
|
||||
if userRole == string(entity.RoleAdmin) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 普通用户只能访问自己的 workspace
|
||||
if userWorkspaceID != workspaceID {
|
||||
respondError(w, http.StatusForbidden, "Access denied", "")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@ -127,6 +127,69 @@ func (f *AdapterFactory) CreateEntryClient() repository.InstanceEntryClient {
|
||||
return k8s.NewEntryClient()
|
||||
}
|
||||
|
||||
// CreateWorkspaceRepository 创建 Workspace 仓储
|
||||
func (f *AdapterFactory) CreateWorkspaceRepository() (repository.WorkspaceRepository, error) {
|
||||
if f.mode == ModeMock {
|
||||
return nil, fmt.Errorf("workspace repository mock not implemented")
|
||||
}
|
||||
|
||||
// 默认:真实实现(PostgreSQL)
|
||||
if err := f.ensureDBConnection(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return postgres.NewWorkspaceRepository(f.db), nil
|
||||
}
|
||||
|
||||
// CreateQuotaRepository 创建 Quota 仓储
|
||||
// CreateStorageRepository 创建存储后端仓储
|
||||
func (f *AdapterFactory) CreateStorageRepository() (repository.StorageRepository, error) {
|
||||
if f.mode == ModeMock {
|
||||
return mock.NewStorageRepositoryMock(), nil
|
||||
}
|
||||
|
||||
if err := f.ensureDBConnection(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return postgres.NewStorageRepository(f.db), nil
|
||||
}
|
||||
|
||||
// CreateQuotaRepository 创建配额仓储
|
||||
func (f *AdapterFactory) CreateQuotaRepository() (repository.QuotaRepository, error) {
|
||||
if f.mode == ModeMock {
|
||||
return nil, fmt.Errorf("quota repository mock not implemented")
|
||||
}
|
||||
|
||||
// 默认:真实实现(PostgreSQL)
|
||||
if err := f.ensureDBConnection(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return postgres.NewQuotaRepository(f.db), nil
|
||||
}
|
||||
|
||||
// CreateChartReferenceRepository 创建 Chart 引用仓储
|
||||
func (f *AdapterFactory) CreateChartReferenceRepository() (repository.ChartReferenceRepository, error) {
|
||||
if f.mode == ModeMock {
|
||||
return nil, fmt.Errorf("chart reference repository mock not implemented")
|
||||
}
|
||||
|
||||
if err := f.ensureDBConnection(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return postgres.NewChartReferenceRepository(f.db), nil
|
||||
}
|
||||
|
||||
// CreateValuesTemplateRepository 创建 Values 模板仓储
|
||||
func (f *AdapterFactory) CreateValuesTemplateRepository() (repository.ValuesTemplateRepository, error) {
|
||||
if f.mode == ModeMock {
|
||||
return nil, fmt.Errorf("values template repository mock not implemented")
|
||||
}
|
||||
|
||||
if err := f.ensureDBConnection(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return postgres.NewValuesTemplateRepository(f.db), nil
|
||||
}
|
||||
|
||||
// CreateAllRepositories 一次性创建所有 Repositories
|
||||
func (f *AdapterFactory) CreateAllRepositories() (*Repositories, error) {
|
||||
userRepo, err := f.CreateUserRepository()
|
||||
@ -149,6 +212,21 @@ func (f *AdapterFactory) CreateAllRepositories() (*Repositories, error) {
|
||||
return nil, fmt.Errorf("failed to create instance repository: %w", err)
|
||||
}
|
||||
|
||||
workspaceRepo, err := f.CreateWorkspaceRepository()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create workspace repository: %w", err)
|
||||
}
|
||||
|
||||
storageRepo, err := f.CreateStorageRepository()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create storage repository: %w", err)
|
||||
}
|
||||
|
||||
quotaRepo, err := f.CreateQuotaRepository()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create quota repository: %w", err)
|
||||
}
|
||||
|
||||
ociClient, err := f.CreateOCIClient()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create OCI client: %w", err)
|
||||
@ -163,28 +241,48 @@ func (f *AdapterFactory) CreateAllRepositories() (*Repositories, error) {
|
||||
metricsClient := f.CreateMetricsClient(clusterRepo)
|
||||
entryClient := f.CreateEntryClient()
|
||||
|
||||
chartRefRepo, err := f.CreateChartReferenceRepository()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create chart reference repository: %w", err)
|
||||
}
|
||||
|
||||
valuesTemplateRepo, err := f.CreateValuesTemplateRepository()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create values template repository: %w", err)
|
||||
}
|
||||
|
||||
return &Repositories{
|
||||
UserRepo: userRepo,
|
||||
ClusterRepo: clusterRepo,
|
||||
RegistryRepo: registryRepo,
|
||||
InstanceRepo: instanceRepo,
|
||||
OCIClient: ociClient,
|
||||
HelmClient: helmClient,
|
||||
MetricsClient: metricsClient,
|
||||
EntryClient: entryClient,
|
||||
UserRepo: userRepo,
|
||||
ClusterRepo: clusterRepo,
|
||||
RegistryRepo: registryRepo,
|
||||
InstanceRepo: instanceRepo,
|
||||
WorkspaceRepo: workspaceRepo,
|
||||
StorageRepo: storageRepo,
|
||||
ChartRefRepo: chartRefRepo,
|
||||
ValuesTemplateRepo: valuesTemplateRepo,
|
||||
QuotaRepo: quotaRepo,
|
||||
OCIClient: ociClient,
|
||||
HelmClient: helmClient,
|
||||
MetricsClient: metricsClient,
|
||||
EntryClient: entryClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Repositories 所有仓储的集合
|
||||
type Repositories struct {
|
||||
UserRepo repository.UserRepository
|
||||
ClusterRepo repository.ClusterRepository
|
||||
RegistryRepo repository.RegistryRepository
|
||||
InstanceRepo repository.InstanceRepository
|
||||
OCIClient repository.OCIClient
|
||||
HelmClient repository.HelmClient
|
||||
MetricsClient repository.MetricsClient
|
||||
EntryClient repository.InstanceEntryClient
|
||||
UserRepo repository.UserRepository
|
||||
ClusterRepo repository.ClusterRepository
|
||||
RegistryRepo repository.RegistryRepository
|
||||
InstanceRepo repository.InstanceRepository
|
||||
WorkspaceRepo repository.WorkspaceRepository
|
||||
StorageRepo repository.StorageRepository
|
||||
ChartRefRepo repository.ChartReferenceRepository
|
||||
ValuesTemplateRepo repository.ValuesTemplateRepository
|
||||
QuotaRepo repository.QuotaRepository
|
||||
OCIClient repository.OCIClient
|
||||
HelmClient repository.HelmClient
|
||||
MetricsClient repository.MetricsClient
|
||||
EntryClient repository.InstanceEntryClient
|
||||
}
|
||||
|
||||
// ensureDBConnection 确保数据库连接已建立
|
||||
|
||||
@ -262,12 +262,40 @@ func (c *OCIClientMock) GetValuesSchema(ctx context.Context, registry *entity.Re
|
||||
return mockSchema, nil
|
||||
}
|
||||
|
||||
func (c *OCIClientMock) GetValues(ctx context.Context, registry *entity.Registry, repository, reference string) (string, error) {
|
||||
artifact, err := c.GetArtifact(ctx, registry, repository, reference)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !artifact.IsChart() {
|
||||
return "", fmt.Errorf("not a helm chart")
|
||||
}
|
||||
|
||||
// 返回 Mock values.yaml
|
||||
mockValues := `# Default values for the chart
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: nginx
|
||||
tag: latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
|
||||
resources: {}
|
||||
`
|
||||
return mockValues, nil
|
||||
}
|
||||
|
||||
func (c *OCIClientMock) PullArtifact(ctx context.Context, registry *entity.Registry, repository, reference, destPath string) error {
|
||||
_, err := c.GetArtifact(ctx, registry, repository, reference)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
// Mock 实现,不实际下载
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -43,13 +43,26 @@ func (c *OCIClient) getRegistry(reg *entity.Registry) (*remote.Registry, error)
|
||||
return nil, fmt.Errorf("failed to create registry client: %w", err)
|
||||
}
|
||||
|
||||
// 设置认证
|
||||
if reg.Username != "" && reg.Password != "" {
|
||||
// 设置认证 - 优先使用 registry 自己的凭证,否则使用 .env 中的默认凭证
|
||||
username := reg.Username
|
||||
password := reg.Password
|
||||
|
||||
// 如果没有提供凭证,尝试从环境变量加载
|
||||
if (username == "" || password == "") && strings.Contains(reg.URL, "harbor") {
|
||||
if envUser := os.Getenv("HARBOR_USERNAME"); envUser != "" {
|
||||
username = envUser
|
||||
}
|
||||
if envPass := os.Getenv("HARBOR_PASSWORD"); envPass != "" {
|
||||
password = envPass
|
||||
}
|
||||
}
|
||||
|
||||
if username != "" && password != "" {
|
||||
registry.Client = &auth.Client{
|
||||
Client: c.httpClient,
|
||||
Credential: auth.StaticCredential(registryURL, auth.Credential{
|
||||
Username: reg.Username,
|
||||
Password: reg.Password,
|
||||
Username: username,
|
||||
Password: password,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@ -370,6 +383,105 @@ func (c *OCIClient) GetValuesSchema(ctx context.Context, registry *entity.Regist
|
||||
return "", entity.ErrValuesSchemaNotFound
|
||||
}
|
||||
|
||||
// GetValues 获取 Helm Chart 的 values.yaml
|
||||
func (c *OCIClient) GetValues(ctx context.Context, registry *entity.Registry, repository, reference string) (string, error) {
|
||||
reg, err := c.getRegistry(registry)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
repo, err := reg.Repository(ctx, repository)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get repository: %w", err)
|
||||
}
|
||||
|
||||
// 解析 reference (tag 或 digest)
|
||||
desc, err := repo.Resolve(ctx, reference)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to resolve artifact: %w", err)
|
||||
}
|
||||
|
||||
manifestReader, err := repo.Fetch(ctx, desc)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to fetch manifest: %w", err)
|
||||
}
|
||||
defer manifestReader.Close()
|
||||
|
||||
manifestBytes, err := io.ReadAll(manifestReader)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read manifest: %w", err)
|
||||
}
|
||||
|
||||
var manifest ocispec.Manifest
|
||||
if err := json.Unmarshal(manifestBytes, &manifest); err != nil {
|
||||
return "", fmt.Errorf("failed to unmarshal manifest: %w", err)
|
||||
}
|
||||
|
||||
// 查找 Helm Chart layer(tar+gzip 包含 chart 内容)并从中读取 values.yaml
|
||||
var chartLayer *ocispec.Descriptor
|
||||
for i := range manifest.Layers {
|
||||
layer := manifest.Layers[i]
|
||||
if strings.Contains(layer.MediaType, "cncf.helm.chart") ||
|
||||
strings.Contains(layer.MediaType, "helm.chart.content") {
|
||||
chartLayer = &manifest.Layers[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if chartLayer == nil {
|
||||
return "", entity.ErrValuesNotFound
|
||||
}
|
||||
|
||||
if chartLayer.Digest == "" {
|
||||
return "", fmt.Errorf("chart layer digest is empty")
|
||||
}
|
||||
if _, err := digest.Parse(string(chartLayer.Digest)); err != nil {
|
||||
return "", fmt.Errorf("invalid chart layer digest: %w", err)
|
||||
}
|
||||
|
||||
layerReader, err := repo.Fetch(ctx, *chartLayer)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to fetch chart layer: %w", err)
|
||||
}
|
||||
defer layerReader.Close()
|
||||
|
||||
gzipReader, err := gzip.NewReader(layerReader)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create gzip reader: %w", err)
|
||||
}
|
||||
defer gzipReader.Close()
|
||||
|
||||
tarReader := tar.NewReader(gzipReader)
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read chart archive: %w", err)
|
||||
}
|
||||
|
||||
if header.Typeflag != tar.TypeReg {
|
||||
continue
|
||||
}
|
||||
|
||||
// 查找 values.yaml 文件(可能在 chart 根目录或子目录中)
|
||||
// 通常路径格式为: {chart-name}/values.yaml
|
||||
if strings.HasSuffix(header.Name, "values.yaml") {
|
||||
data, err := io.ReadAll(tarReader)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read values.yaml: %w", err)
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return "", entity.ErrValuesNotFound
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", entity.ErrValuesNotFound
|
||||
}
|
||||
|
||||
// PullArtifact 下载 artifact 到本地
|
||||
func (c *OCIClient) PullArtifact(ctx context.Context, registry *entity.Registry, repository, reference, destPath string) error {
|
||||
reg, err := c.getRegistry(registry)
|
||||
|
||||
@ -104,13 +104,41 @@ func (r *ClusterRepositoryMock) Delete(ctx context.Context, id string) error {
|
||||
func (r *ClusterRepositoryMock) List(ctx context.Context) ([]*entity.Cluster, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
|
||||
clusters := make([]*entity.Cluster, 0, len(r.clusters))
|
||||
for _, cluster := range r.clusters {
|
||||
// 解密敏感数据后返回
|
||||
clusters = append(clusters, r.decryptCluster(cluster))
|
||||
}
|
||||
|
||||
|
||||
return clusters, nil
|
||||
}
|
||||
|
||||
func (r *ClusterRepositoryMock) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.Cluster, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
clusters := make([]*entity.Cluster, 0)
|
||||
for _, cluster := range r.clusters {
|
||||
if cluster.WorkspaceID == workspaceID {
|
||||
clusters = append(clusters, r.decryptCluster(cluster))
|
||||
}
|
||||
}
|
||||
|
||||
return clusters, nil
|
||||
}
|
||||
|
||||
func (r *ClusterRepositoryMock) GetShared(ctx context.Context) ([]*entity.Cluster, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
clusters := make([]*entity.Cluster, 0)
|
||||
for _, cluster := range r.clusters {
|
||||
if cluster.IsShared {
|
||||
clusters = append(clusters, r.decryptCluster(cluster))
|
||||
}
|
||||
}
|
||||
|
||||
return clusters, nil
|
||||
}
|
||||
|
||||
|
||||
@ -102,12 +102,26 @@ func (r *InstanceRepositoryMock) ListByCluster(ctx context.Context, clusterID st
|
||||
func (r *InstanceRepositoryMock) List(ctx context.Context) ([]*entity.Instance, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
|
||||
instances := make([]*entity.Instance, 0, len(r.instances))
|
||||
for _, instance := range r.instances {
|
||||
instances = append(instances, instance)
|
||||
}
|
||||
|
||||
|
||||
return instances, nil
|
||||
}
|
||||
|
||||
func (r *InstanceRepositoryMock) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.Instance, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
instances := make([]*entity.Instance, 0)
|
||||
for _, instance := range r.instances {
|
||||
if instance.WorkspaceID == workspaceID {
|
||||
instances = append(instances, instance)
|
||||
}
|
||||
}
|
||||
|
||||
return instances, nil
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,96 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
)
|
||||
|
||||
// StorageRepositoryMock Storage 仓储 Mock
|
||||
type StorageRepositoryMock struct {
|
||||
storages map[string]*entity.StorageBackend
|
||||
}
|
||||
|
||||
// NewStorageRepositoryMock 创建 Mock
|
||||
func NewStorageRepositoryMock() *StorageRepositoryMock {
|
||||
return &StorageRepositoryMock{
|
||||
storages: make(map[string]*entity.StorageBackend),
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建存储
|
||||
func (r *StorageRepositoryMock) Create(ctx context.Context, storage *entity.StorageBackend) error {
|
||||
r.storages[storage.ID] = storage
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 获取存储
|
||||
func (r *StorageRepositoryMock) GetByID(ctx context.Context, id string) (*entity.StorageBackend, error) {
|
||||
if s, ok := r.storages[id]; ok {
|
||||
return s, nil
|
||||
}
|
||||
return nil, entity.ErrStorageNotFound
|
||||
}
|
||||
|
||||
// GetByWorkspace 获取工作空间的存储
|
||||
func (r *StorageRepositoryMock) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.StorageBackend, error) {
|
||||
var result []*entity.StorageBackend
|
||||
for _, s := range r.storages {
|
||||
if s.WorkspaceID == workspaceID {
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetByName 按名称获取
|
||||
func (r *StorageRepositoryMock) GetByName(ctx context.Context, workspaceID, name string) (*entity.StorageBackend, error) {
|
||||
for _, s := range r.storages {
|
||||
if s.WorkspaceID == workspaceID && s.Name == name {
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
return nil, entity.ErrStorageNotFound
|
||||
}
|
||||
|
||||
// Update 更新存储
|
||||
func (r *StorageRepositoryMock) Update(ctx context.Context, storage *entity.StorageBackend) error {
|
||||
r.storages[storage.ID] = storage
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除存储
|
||||
func (r *StorageRepositoryMock) Delete(ctx context.Context, id string) error {
|
||||
delete(r.storages, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetShared 获取所有共享存储后端
|
||||
func (r *StorageRepositoryMock) GetShared(ctx context.Context) ([]*entity.StorageBackend, error) {
|
||||
var result []*entity.StorageBackend
|
||||
for _, s := range r.storages {
|
||||
if s.IsShared {
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetDefault 获取 workspace 的默认存储后端
|
||||
func (r *StorageRepositoryMock) GetDefault(ctx context.Context, workspaceID string) (*entity.StorageBackend, error) {
|
||||
for _, s := range r.storages {
|
||||
if s.WorkspaceID == workspaceID && s.IsDefault {
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
return nil, entity.ErrStorageNotFound
|
||||
}
|
||||
|
||||
// List 列出所有存储(管理员用)
|
||||
func (r *StorageRepositoryMock) List(ctx context.Context) ([]*entity.StorageBackend, error) {
|
||||
var result []*entity.StorageBackend
|
||||
for _, s := range r.storages {
|
||||
result = append(result, s)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@ -88,12 +88,40 @@ func (r *UserRepositoryMock) Delete(ctx context.Context, id string) error {
|
||||
func (r *UserRepositoryMock) List(ctx context.Context) ([]*entity.User, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
|
||||
users := make([]*entity.User, 0, len(r.users))
|
||||
for _, user := range r.users {
|
||||
users = append(users, user)
|
||||
}
|
||||
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *UserRepositoryMock) ListByWorkspace(ctx context.Context, workspaceID string) ([]*entity.User, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
users := make([]*entity.User, 0)
|
||||
for _, user := range r.users {
|
||||
if user.WorkspaceID == workspaceID {
|
||||
users = append(users, user)
|
||||
}
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *UserRepositoryMock) ListActive(ctx context.Context) ([]*entity.User, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
users := make([]*entity.User, 0)
|
||||
for _, user := range r.users {
|
||||
if user.IsActive {
|
||||
users = append(users, user)
|
||||
}
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,200 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/repository"
|
||||
)
|
||||
|
||||
// AuditLogRepository PostgreSQL 审计日志仓储实现
|
||||
type AuditLogRepository struct {
|
||||
db *DB
|
||||
}
|
||||
|
||||
// NewAuditLogRepository 创建 PostgreSQL 审计日志仓储
|
||||
func NewAuditLogRepository(db *DB) repository.AuditLogRepository {
|
||||
return &AuditLogRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建审计日志
|
||||
func (r *AuditLogRepository) Create(ctx context.Context, log *entity.AuditLog) error {
|
||||
if log.ID == "" {
|
||||
log.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
detailsJSON, err := json.Marshal(log.Details)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal details: %w", err)
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO audit_logs (id, workspace_id, user_id, action, resource_type, resource_id, resource_name, details, ip_address, user_agent, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
`
|
||||
|
||||
_, err = r.db.conn.ExecContext(ctx, query,
|
||||
log.ID,
|
||||
log.WorkspaceID,
|
||||
log.UserID,
|
||||
log.Action,
|
||||
log.ResourceType,
|
||||
log.ResourceID,
|
||||
log.ResourceName,
|
||||
detailsJSON,
|
||||
log.IPAddress,
|
||||
log.UserAgent,
|
||||
log.CreatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create audit log: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByWorkspace 获取 workspace 的审计日志
|
||||
func (r *AuditLogRepository) GetByWorkspace(ctx context.Context, workspaceID string, limit int) ([]*entity.AuditLog, error) {
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT id, workspace_id, user_id, action, resource_type, resource_id, resource_name, details, ip_address, user_agent, created_at
|
||||
FROM audit_logs
|
||||
WHERE workspace_id = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, workspaceID, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get audit logs: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanAuditLogs(rows)
|
||||
}
|
||||
|
||||
// GetByUser 获取用户的审计日志
|
||||
func (r *AuditLogRepository) GetByUser(ctx context.Context, userID string, limit int) ([]*entity.AuditLog, error) {
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT id, workspace_id, user_id, action, resource_type, resource_id, resource_name, details, ip_address, user_agent, created_at
|
||||
FROM audit_logs
|
||||
WHERE user_id = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, userID, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get audit logs: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanAuditLogs(rows)
|
||||
}
|
||||
|
||||
// GetByResource 获取资源的审计日志
|
||||
func (r *AuditLogRepository) GetByResource(ctx context.Context, resourceType entity.AuditResourceType, resourceID string, limit int) ([]*entity.AuditLog, error) {
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT id, workspace_id, user_id, admin action, resource_type, resource_id, resource_name, details, ip_address, user_agent, created_at
|
||||
FROM audit_logs
|
||||
WHERE resource_type = $1 AND resource_id = $2
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $3
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, resourceType, resourceID, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get audit logs: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanAuditLogs(rows)
|
||||
}
|
||||
|
||||
// List 列出审计日志(分页)
|
||||
func (r *AuditLogRepository) List(ctx context.Context, limit, offset int) ([]*entity.AuditLog, error) {
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT id, workspace_id, user_id, action, resource_type, resource_id, resource_name, details, ip_address, user_agent, created_at
|
||||
FROM audit_logs
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $1 OFFSET $2
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, limit, offset)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list audit logs: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanAuditLogs(rows)
|
||||
}
|
||||
|
||||
// DeleteByWorkspace 删除 workspace 的审计日志
|
||||
func (r *AuditLogRepository) DeleteByWorkspace(ctx context.Context, workspaceID string) error {
|
||||
query := `DELETE FROM audit_logs WHERE workspace_id = $1`
|
||||
|
||||
_, err := r.db.conn.ExecContext(ctx, query, workspaceID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete audit logs: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// scanAuditLogs 扫描多行结果
|
||||
func (r *AuditLogRepository) scanAuditLogs(rows *sql.Rows) ([]*entity.AuditLog, error) {
|
||||
logs := make([]*entity.AuditLog, 0)
|
||||
for rows.Next() {
|
||||
log := &entity.AuditLog{}
|
||||
var detailsJSON []byte
|
||||
err := rows.Scan(
|
||||
&log.ID,
|
||||
&log.WorkspaceID,
|
||||
&log.UserID,
|
||||
&log.Action,
|
||||
&log.ResourceType,
|
||||
&log.ResourceID,
|
||||
&log.ResourceName,
|
||||
&detailsJSON,
|
||||
&log.IPAddress,
|
||||
&log.UserAgent,
|
||||
&log.CreatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan audit log: %w", err)
|
||||
}
|
||||
if err := json.Unmarshal(detailsJSON, &log.Details); err != nil {
|
||||
log.Details = make(map[string]interface{})
|
||||
}
|
||||
logs = append(logs, log)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
return logs, nil
|
||||
}
|
||||
@ -0,0 +1,253 @@
|
||||
package postgres
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// ChartReferenceRepository PostgreSQL Chart 引用仓储实现
|
||||
type ChartReferenceRepository struct {
|
||||
db *DB
|
||||
}
|
||||
|
||||
// NewChartReferenceRepository 创建 PostgreSQL Chart 引用仓储
|
||||
func NewChartReferenceRepository(db *DB) repository.ChartReferenceRepository {
|
||||
return &ChartReferenceRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建 Chart 引用
|
||||
func (r *ChartReferenceRepository) Create(ctx context.Context, chartRef *entity.ChartReference) error {
|
||||
if chartRef.ID == "" {
|
||||
chartRef.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO chart_references (id, workspace_id, registry_id, repository, chart_name, description, is_enabled, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
`
|
||||
|
||||
_, err := r.db.conn.ExecContext(ctx, query,
|
||||
chartRef.ID,
|
||||
chartRef.WorkspaceID,
|
||||
chartRef.RegistryID,
|
||||
chartRef.Repository,
|
||||
chartRef.ChartName,
|
||||
chartRef.Description,
|
||||
chartRef.IsEnabled,
|
||||
chartRef.CreatedAt,
|
||||
chartRef.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create chart reference: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 根据 ID 获取 Chart 引用
|
||||
func (r *ChartReferenceRepository) GetByID(ctx context.Context, id string) (*entity.ChartReference, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, registry_id, repository, chart_name, description, is_enabled, created_at, updated_at
|
||||
FROM chart_references
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
chartRef := &entity.ChartReference{}
|
||||
err := r.db.conn.QueryRowContext(ctx, query, id).Scan(
|
||||
&chartRef.ID,
|
||||
&chartRef.WorkspaceID,
|
||||
&chartRef.RegistryID,
|
||||
&chartRef.Repository,
|
||||
&chartRef.ChartName,
|
||||
&chartRef.Description,
|
||||
&chartRef.IsEnabled,
|
||||
&chartRef.CreatedAt,
|
||||
&chartRef.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, entity.ErrChartReferenceNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get chart reference: %w", err)
|
||||
}
|
||||
|
||||
return chartRef, nil
|
||||
}
|
||||
|
||||
// GetByWorkspace 获取 workspace 的所有 Chart 引用
|
||||
func (r *ChartReferenceRepository) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.ChartReference, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, registry_id, repository, chart_name, description, is_enabled, created_at, updated_at
|
||||
FROM chart_references
|
||||
WHERE workspace_id = $1
|
||||
ORDER BY chart_name
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, workspaceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list chart references: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanChartReferences(rows)
|
||||
}
|
||||
|
||||
// GetByRegistry 获取 registry 的所有 Chart 引用
|
||||
func (r *ChartReferenceRepository) GetByRegistry(ctx context.Context, registryID string) ([]*entity.ChartReference, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, registry_id, repository, chart_name, description, is_enabled, created_at, updated_at
|
||||
FROM chart_references
|
||||
WHERE registry_id = $1
|
||||
ORDER BY chart_name
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, registryID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list chart references: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanChartReferences(rows)
|
||||
}
|
||||
|
||||
// GetByName 根据名称获取 Chart 引用
|
||||
func (r *ChartReferenceRepository) GetByName(ctx context.Context, workspaceID, chartName string) (*entity.ChartReference, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, registry_id, repository, chart_name, description, is_enabled, created_at, updated_at
|
||||
FROM chart_references
|
||||
WHERE workspace_id = $1 AND chart_name = $2
|
||||
`
|
||||
|
||||
chartRef := &entity.ChartReference{}
|
||||
err := r.db.conn.QueryRowContext(ctx, query, workspaceID, chartName).Scan(
|
||||
&chartRef.ID,
|
||||
&chartRef.WorkspaceID,
|
||||
&chartRef.RegistryID,
|
||||
&chartRef.Repository,
|
||||
&chartRef.ChartName,
|
||||
&chartRef.Description,
|
||||
&chartRef.IsEnabled,
|
||||
&chartRef.CreatedAt,
|
||||
&chartRef.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, entity.ErrChartReferenceNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get chart reference: %w", err)
|
||||
}
|
||||
|
||||
return chartRef, nil
|
||||
}
|
||||
|
||||
// Update 更新 Chart 引用
|
||||
func (r *ChartReferenceRepository) Update(ctx context.Context, chartRef *entity.ChartReference) error {
|
||||
chartRef.UpdatedAt = time.Now()
|
||||
|
||||
query := `
|
||||
UPDATE chart_references
|
||||
SET registry_id = $1, repository = $2, chart_name = $3, description = $4, is_enabled = $5, updated_at = $6
|
||||
WHERE id = $7
|
||||
`
|
||||
|
||||
result, err := r.db.conn.ExecContext(ctx, query,
|
||||
chartRef.RegistryID,
|
||||
chartRef.Repository,
|
||||
chartRef.ChartName,
|
||||
chartRef.Description,
|
||||
chartRef.IsEnabled,
|
||||
chartRef.UpdatedAt,
|
||||
chartRef.ID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update chart reference: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get affected rows: %w", err)
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return entity.ErrChartReferenceNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除 Chart 引用
|
||||
func (r *ChartReferenceRepository) Delete(ctx context.Context, id string) error {
|
||||
query := `DELETE FROM chart_references WHERE id = $1`
|
||||
|
||||
result, err := r.db.conn.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete chart reference: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get affected rows: %w", err)
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return entity.ErrChartReferenceNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// List 列出所有 Chart 引用(管理员用)
|
||||
func (r *ChartReferenceRepository) List(ctx context.Context) ([]*entity.ChartReference, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, registry_id, repository, chart_name, description, is_enabled, created_at, updated_at
|
||||
FROM chart_references
|
||||
ORDER BY workspace_id, chart_name
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list chart references: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanChartReferences(rows)
|
||||
}
|
||||
|
||||
// scanChartReferences 扫描多行结果
|
||||
func (r *ChartReferenceRepository) scanChartReferences(rows *sql.Rows) ([]*entity.ChartReference, error) {
|
||||
chartRefs := make([]*entity.ChartReference, 0)
|
||||
for rows.Next() {
|
||||
chartRef := &entity.ChartReference{}
|
||||
err := rows.Scan(
|
||||
&chartRef.ID,
|
||||
&chartRef.WorkspaceID,
|
||||
&chartRef.RegistryID,
|
||||
&chartRef.Repository,
|
||||
&chartRef.ChartName,
|
||||
&chartRef.Description,
|
||||
&chartRef.IsEnabled,
|
||||
&chartRef.CreatedAt,
|
||||
&chartRef.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan chart reference: %w", err)
|
||||
}
|
||||
chartRefs = append(chartRefs, chartRef)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
return chartRefs, nil
|
||||
}
|
||||
@ -32,6 +32,11 @@ func (r *ClusterRepository) Create(ctx context.Context, cluster *entity.Cluster)
|
||||
cluster.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
if cluster.IsolationMode == "" {
|
||||
cluster.IsolationMode = entity.IsolationModeNamespace
|
||||
}
|
||||
|
||||
// 加密敏感数据
|
||||
encryptedCAData, err := r.encryptor.Encrypt(cluster.CAData)
|
||||
if err != nil {
|
||||
@ -54,12 +59,14 @@ func (r *ClusterRepository) Create(ctx context.Context, cluster *entity.Cluster)
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO clusters (id, name, host, ca_data, cert_data, key_data, token, description, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
INSERT INTO clusters (id, workspace_id, owner_id, name, host, ca_data, cert_data, key_data, token, description, isolation_mode, default_namespace, is_shared, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||
`
|
||||
|
||||
_, err = r.db.conn.ExecContext(ctx, query,
|
||||
cluster.ID,
|
||||
cluster.WorkspaceID,
|
||||
cluster.OwnerID,
|
||||
cluster.Name,
|
||||
cluster.Host,
|
||||
encryptedCAData,
|
||||
@ -67,6 +74,9 @@ func (r *ClusterRepository) Create(ctx context.Context, cluster *entity.Cluster)
|
||||
encryptedKeyData,
|
||||
encryptedToken,
|
||||
cluster.Description,
|
||||
cluster.IsolationMode,
|
||||
cluster.DefaultNamespace,
|
||||
cluster.IsShared,
|
||||
cluster.CreatedAt,
|
||||
cluster.UpdatedAt,
|
||||
)
|
||||
@ -81,7 +91,7 @@ func (r *ClusterRepository) Create(ctx context.Context, cluster *entity.Cluster)
|
||||
// GetByID 根据 ID 获取集群
|
||||
func (r *ClusterRepository) GetByID(ctx context.Context, id string) (*entity.Cluster, error) {
|
||||
query := `
|
||||
SELECT id, name, host, ca_data, cert_data, key_data, token, description, created_at, updated_at
|
||||
SELECT id, workspace_id, owner_id, name, host, ca_data, cert_data, key_data, token, description, isolation_mode, default_namespace, is_shared, created_at, updated_at
|
||||
FROM clusters
|
||||
WHERE id = $1
|
||||
`
|
||||
@ -91,6 +101,8 @@ func (r *ClusterRepository) GetByID(ctx context.Context, id string) (*entity.Clu
|
||||
|
||||
err := r.db.conn.QueryRowContext(ctx, query, id).Scan(
|
||||
&cluster.ID,
|
||||
&cluster.WorkspaceID,
|
||||
&cluster.OwnerID,
|
||||
&cluster.Name,
|
||||
&cluster.Host,
|
||||
&encryptedCAData,
|
||||
@ -98,6 +110,9 @@ func (r *ClusterRepository) GetByID(ctx context.Context, id string) (*entity.Clu
|
||||
&encryptedKeyData,
|
||||
&encryptedToken,
|
||||
&cluster.Description,
|
||||
&cluster.IsolationMode,
|
||||
&cluster.DefaultNamespace,
|
||||
&cluster.IsShared,
|
||||
&cluster.CreatedAt,
|
||||
&cluster.UpdatedAt,
|
||||
)
|
||||
@ -110,25 +125,10 @@ func (r *ClusterRepository) GetByID(ctx context.Context, id string) (*entity.Clu
|
||||
}
|
||||
|
||||
// 解密敏感数据
|
||||
cluster.CAData, err = r.encryptor.Decrypt(encryptedCAData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt CA data: %w", err)
|
||||
}
|
||||
|
||||
cluster.CertData, err = r.encryptor.Decrypt(encryptedCertData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt cert data: %w", err)
|
||||
}
|
||||
|
||||
cluster.KeyData, err = r.encryptor.Decrypt(encryptedKeyData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt key data: %w", err)
|
||||
}
|
||||
|
||||
cluster.Token, err = r.encryptor.Decrypt(encryptedToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt token: %w", err)
|
||||
}
|
||||
cluster.CAData, _ = r.encryptor.Decrypt(encryptedCAData)
|
||||
cluster.CertData, _ = r.encryptor.Decrypt(encryptedCertData)
|
||||
cluster.KeyData, _ = r.encryptor.Decrypt(encryptedKeyData)
|
||||
cluster.Token, _ = r.encryptor.Decrypt(encryptedToken)
|
||||
|
||||
return cluster, nil
|
||||
}
|
||||
@ -136,7 +136,7 @@ func (r *ClusterRepository) GetByID(ctx context.Context, id string) (*entity.Clu
|
||||
// GetByName 根据名称获取集群
|
||||
func (r *ClusterRepository) GetByName(ctx context.Context, name string) (*entity.Cluster, error) {
|
||||
query := `
|
||||
SELECT id, name, host, ca_data, cert_data, key_data, token, description, created_at, updated_at
|
||||
SELECT id, workspace_id, owner_id, name, host, ca_data, cert_data, key_data, token, description, isolation_mode, default_namespace, is_shared, created_at, updated_at
|
||||
FROM clusters
|
||||
WHERE name = $1
|
||||
`
|
||||
@ -146,6 +146,8 @@ func (r *ClusterRepository) GetByName(ctx context.Context, name string) (*entity
|
||||
|
||||
err := r.db.conn.QueryRowContext(ctx, query, name).Scan(
|
||||
&cluster.ID,
|
||||
&cluster.WorkspaceID,
|
||||
&cluster.OwnerID,
|
||||
&cluster.Name,
|
||||
&cluster.Host,
|
||||
&encryptedCAData,
|
||||
@ -153,6 +155,9 @@ func (r *ClusterRepository) GetByName(ctx context.Context, name string) (*entity
|
||||
&encryptedKeyData,
|
||||
&encryptedToken,
|
||||
&cluster.Description,
|
||||
&cluster.IsolationMode,
|
||||
&cluster.DefaultNamespace,
|
||||
&cluster.IsShared,
|
||||
&cluster.CreatedAt,
|
||||
&cluster.UpdatedAt,
|
||||
)
|
||||
@ -165,25 +170,10 @@ func (r *ClusterRepository) GetByName(ctx context.Context, name string) (*entity
|
||||
}
|
||||
|
||||
// 解密敏感数据
|
||||
cluster.CAData, err = r.encryptor.Decrypt(encryptedCAData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt CA data: %w", err)
|
||||
}
|
||||
|
||||
cluster.CertData, err = r.encryptor.Decrypt(encryptedCertData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt cert data: %w", err)
|
||||
}
|
||||
|
||||
cluster.KeyData, err = r.encryptor.Decrypt(encryptedKeyData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt key data: %w", err)
|
||||
}
|
||||
|
||||
cluster.Token, err = r.encryptor.Decrypt(encryptedToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt token: %w", err)
|
||||
}
|
||||
cluster.CAData, _ = r.encryptor.Decrypt(encryptedCAData)
|
||||
cluster.CertData, _ = r.encryptor.Decrypt(encryptedCertData)
|
||||
cluster.KeyData, _ = r.encryptor.Decrypt(encryptedKeyData)
|
||||
cluster.Token, _ = r.encryptor.Decrypt(encryptedToken)
|
||||
|
||||
return cluster, nil
|
||||
}
|
||||
@ -215,9 +205,10 @@ func (r *ClusterRepository) Update(ctx context.Context, cluster *entity.Cluster)
|
||||
|
||||
query := `
|
||||
UPDATE clusters
|
||||
SET name = $1, host = $2, ca_data = $3, cert_data = $4, key_data = $5,
|
||||
token = $6, description = $7, updated_at = $8
|
||||
WHERE id = $9
|
||||
SET name = $1, host = $2, ca_data = $3, cert_data = $4, key_data = $5,
|
||||
token = $6, description = $7, isolation_mode = $8, default_namespace = $9,
|
||||
is_shared = $10, updated_at = $11
|
||||
WHERE id = $12
|
||||
`
|
||||
|
||||
result, err := r.db.conn.ExecContext(ctx, query,
|
||||
@ -228,6 +219,9 @@ func (r *ClusterRepository) Update(ctx context.Context, cluster *entity.Cluster)
|
||||
encryptedKeyData,
|
||||
encryptedToken,
|
||||
cluster.Description,
|
||||
cluster.IsolationMode,
|
||||
cluster.DefaultNamespace,
|
||||
cluster.IsShared,
|
||||
cluster.UpdatedAt,
|
||||
cluster.ID,
|
||||
)
|
||||
@ -272,7 +266,7 @@ func (r *ClusterRepository) Delete(ctx context.Context, id string) error {
|
||||
// List 列出所有集群
|
||||
func (r *ClusterRepository) List(ctx context.Context) ([]*entity.Cluster, error) {
|
||||
query := `
|
||||
SELECT id, name, host, ca_data, cert_data, key_data, token, description, created_at, updated_at
|
||||
SELECT id, workspace_id, owner_id, name, host, ca_data, cert_data, key_data, token, description, isolation_mode, default_namespace, is_shared, created_at, updated_at
|
||||
FROM clusters
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
@ -283,13 +277,59 @@ func (r *ClusterRepository) List(ctx context.Context) ([]*entity.Cluster, error)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanClusters(rows)
|
||||
}
|
||||
|
||||
// GetByWorkspace 获取 workspace 的所有集群(包括共享集群)
|
||||
func (r *ClusterRepository) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.Cluster, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, name, host, ca_data, cert_data, key_data, token, description, isolation_mode, default_namespace, is_shared, created_at, updated_at
|
||||
FROM clusters
|
||||
WHERE workspace_id = $1 OR is_shared = TRUE
|
||||
ORDER BY is_shared, created_at DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, workspaceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list clusters by workspace: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanClusters(rows)
|
||||
}
|
||||
|
||||
// GetShared 获取所有共享集群
|
||||
func (r *ClusterRepository) GetShared(ctx context.Context) ([]*entity.Cluster, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, name, host, ca_data, cert_data, key_data, token, description, isolation_mode, default_namespace, is_shared, created_at, updated_at
|
||||
FROM clusters
|
||||
WHERE is_shared = TRUE
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list shared clusters: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanClusters(rows)
|
||||
}
|
||||
|
||||
// scanClusters 扫描多行结果
|
||||
func (r *ClusterRepository) scanClusters(rows *sql.Rows) ([]*entity.Cluster, error) {
|
||||
clusters := make([]*entity.Cluster, 0)
|
||||
for rows.Next() {
|
||||
cluster := &entity.Cluster{}
|
||||
var encryptedCAData, encryptedCertData, encryptedKeyData, encryptedToken string
|
||||
var (
|
||||
encryptedCAData, encryptedCertData, encryptedKeyData, encryptedToken sql.NullString
|
||||
workspaceID, ownerID, defaultNamespace sql.NullString
|
||||
)
|
||||
|
||||
err := rows.Scan(
|
||||
&cluster.ID,
|
||||
&workspaceID,
|
||||
&ownerID,
|
||||
&cluster.Name,
|
||||
&cluster.Host,
|
||||
&encryptedCAData,
|
||||
@ -297,6 +337,9 @@ func (r *ClusterRepository) List(ctx context.Context) ([]*entity.Cluster, error)
|
||||
&encryptedKeyData,
|
||||
&encryptedToken,
|
||||
&cluster.Description,
|
||||
&cluster.IsolationMode,
|
||||
&defaultNamespace,
|
||||
&cluster.IsShared,
|
||||
&cluster.CreatedAt,
|
||||
&cluster.UpdatedAt,
|
||||
)
|
||||
@ -304,25 +347,23 @@ func (r *ClusterRepository) List(ctx context.Context) ([]*entity.Cluster, error)
|
||||
return nil, fmt.Errorf("failed to scan cluster: %w", err)
|
||||
}
|
||||
|
||||
// 处理 NULL 值
|
||||
cluster.WorkspaceID = workspaceID.String
|
||||
cluster.OwnerID = ownerID.String
|
||||
cluster.DefaultNamespace = defaultNamespace.String
|
||||
|
||||
// 解密敏感数据
|
||||
cluster.CAData, err = r.encryptor.Decrypt(encryptedCAData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt CA data: %w", err)
|
||||
if encryptedCAData.Valid {
|
||||
cluster.CAData, _ = r.encryptor.Decrypt(encryptedCAData.String)
|
||||
}
|
||||
|
||||
cluster.CertData, err = r.encryptor.Decrypt(encryptedCertData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt cert data: %w", err)
|
||||
if encryptedCertData.Valid {
|
||||
cluster.CertData, _ = r.encryptor.Decrypt(encryptedCertData.String)
|
||||
}
|
||||
|
||||
cluster.KeyData, err = r.encryptor.Decrypt(encryptedKeyData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt key data: %w", err)
|
||||
if encryptedKeyData.Valid {
|
||||
cluster.KeyData, _ = r.encryptor.Decrypt(encryptedKeyData.String)
|
||||
}
|
||||
|
||||
cluster.Token, err = r.encryptor.Decrypt(encryptedToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt token: %w", err)
|
||||
if encryptedToken.Valid {
|
||||
cluster.Token, _ = r.encryptor.Decrypt(encryptedToken.String)
|
||||
}
|
||||
|
||||
clusters = append(clusters, cluster)
|
||||
|
||||
@ -124,6 +124,58 @@ func (db *DB) InitSchema() error {
|
||||
CREATE INDEX IF NOT EXISTS idx_instances_cluster ON instances(cluster_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_instances_registry ON instances(registry_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_instances_name ON instances(name);
|
||||
|
||||
-- Storage Backends 表
|
||||
CREATE TABLE IF NOT EXISTS storage_backends (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
workspace_id VARCHAR(36),
|
||||
owner_id VARCHAR(36),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
config JSONB NOT NULL,
|
||||
description TEXT,
|
||||
is_default BOOLEAN DEFAULT FALSE,
|
||||
is_shared BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_storage_workspace ON storage_backends(workspace_id);
|
||||
|
||||
-- Chart References 表
|
||||
CREATE TABLE IF NOT EXISTS chart_references (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
workspace_id VARCHAR(36),
|
||||
registry_id VARCHAR(36),
|
||||
repository VARCHAR(500) NOT NULL,
|
||||
chart_name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
is_enabled BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_chart_workspace ON chart_references(workspace_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_chart_registry ON chart_references(registry_id);
|
||||
|
||||
-- Values Templates 表 - 使用复合唯一键替代主键,允许同一模板的多个版本
|
||||
CREATE TABLE IF NOT EXISTS values_templates (
|
||||
id VARCHAR(36),
|
||||
workspace_id VARCHAR(36),
|
||||
owner_id VARCHAR(36),
|
||||
chart_reference_id VARCHAR(36),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
values_yaml TEXT NOT NULL,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
is_default BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE (chart_reference_id, name, version)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_values_template_chart ON values_templates(chart_reference_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_values_template_workspace ON values_templates(workspace_id);
|
||||
`
|
||||
|
||||
_, err := db.conn.Exec(schema)
|
||||
|
||||
@ -431,3 +431,105 @@ func (r *InstanceRepository) List(ctx context.Context) ([]*entity.Instance, erro
|
||||
|
||||
return instances, nil
|
||||
}
|
||||
|
||||
// GetByWorkspace 列出指定工作空间的所有实例(用于配额检查)
|
||||
func (r *InstanceRepository) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.Instance, error) {
|
||||
query := `
|
||||
SELECT id, cluster_id, workspace_id, owner_id, name, namespace, registry_id, repository, chart, version,
|
||||
description, values, values_yaml, values_template_id, user_override_yaml,
|
||||
status, status_reason, last_operation, last_error, revision,
|
||||
cpu_requested, memory_requested, gpu_requested, gpu_memory_requested,
|
||||
created_at, updated_at
|
||||
FROM instances
|
||||
WHERE workspace_id = $1
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, workspaceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get instances by workspace: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
instances := make([]*entity.Instance, 0)
|
||||
for rows.Next() {
|
||||
instance := &entity.Instance{}
|
||||
var (
|
||||
valuesJSON []byte
|
||||
statusReason sql.NullString
|
||||
lastOperation sql.NullString
|
||||
lastError sql.NullString
|
||||
valuesTemplateID sql.NullString
|
||||
userOverrideYAML sql.NullString
|
||||
memoryRequested sql.NullString
|
||||
gpuMemoryRequested sql.NullString
|
||||
)
|
||||
|
||||
err := rows.Scan(
|
||||
&instance.ID,
|
||||
&instance.ClusterID,
|
||||
&instance.WorkspaceID,
|
||||
&instance.OwnerID,
|
||||
&instance.Name,
|
||||
&instance.Namespace,
|
||||
&instance.RegistryID,
|
||||
&instance.Repository,
|
||||
&instance.Chart,
|
||||
&instance.Version,
|
||||
&instance.Description,
|
||||
&valuesJSON,
|
||||
&instance.ValuesYAML,
|
||||
&valuesTemplateID,
|
||||
&userOverrideYAML,
|
||||
&instance.Status,
|
||||
&statusReason,
|
||||
&lastOperation,
|
||||
&lastError,
|
||||
&instance.Revision,
|
||||
&instance.CPURequested,
|
||||
&memoryRequested,
|
||||
&instance.GPURequested,
|
||||
&gpuMemoryRequested,
|
||||
&instance.CreatedAt,
|
||||
&instance.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan instance: %w", err)
|
||||
}
|
||||
|
||||
if valuesJSON != nil {
|
||||
if err := json.Unmarshal(valuesJSON, &instance.Values); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal values: %w", err)
|
||||
}
|
||||
}
|
||||
if valuesTemplateID.Valid {
|
||||
instance.ValuesTemplateID = valuesTemplateID.String
|
||||
}
|
||||
if userOverrideYAML.Valid {
|
||||
instance.UserOverrideYAML = userOverrideYAML.String
|
||||
}
|
||||
if statusReason.Valid {
|
||||
instance.StatusReason = statusReason.String
|
||||
}
|
||||
if lastOperation.Valid {
|
||||
instance.LastOperation = entity.InstanceOperation(lastOperation.String)
|
||||
}
|
||||
if lastError.Valid {
|
||||
instance.LastError = lastError.String
|
||||
}
|
||||
if memoryRequested.Valid {
|
||||
instance.MemoryRequested = memoryRequested.String
|
||||
}
|
||||
if gpuMemoryRequested.Valid {
|
||||
instance.GPUMemoryRequested = gpuMemoryRequested.String
|
||||
}
|
||||
|
||||
instances = append(instances, instance)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
return instances, nil
|
||||
}
|
||||
|
||||
@ -0,0 +1,212 @@
|
||||
package postgres
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// QuotaRepository PostgreSQL 配额仓储实现
|
||||
type QuotaRepository struct {
|
||||
db *DB
|
||||
}
|
||||
|
||||
// NewQuotaRepository 创建 PostgreSQL 配额仓储
|
||||
func NewQuotaRepository(db *DB) repository.QuotaRepository {
|
||||
return &QuotaRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建配额
|
||||
func (r *QuotaRepository) Create(ctx context.Context, quota *entity.WorkspaceQuota) error {
|
||||
if quota.ID == "" {
|
||||
quota.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO workspace_quotas (id, workspace_id, resource_type, hard_limit, soft_limit, used, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
ON CONFLICT (workspace_id, resource_type) DO UPDATE
|
||||
SET hard_limit = $4, soft_limit = $5, updated_at = $8
|
||||
`
|
||||
|
||||
_, err := r.db.conn.ExecContext(ctx, query,
|
||||
quota.ID,
|
||||
quota.WorkspaceID,
|
||||
quota.ResourceType,
|
||||
quota.HardLimit,
|
||||
quota.SoftLimit,
|
||||
quota.Used,
|
||||
quota.CreatedAt,
|
||||
quota.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create quota: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 根据 ID 获取配额
|
||||
func (r *QuotaRepository) GetByID(ctx context.Context, id string) (*entity.WorkspaceQuota, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, resource_type, hard_limit, soft_limit, used, created_at, updated_at
|
||||
FROM workspace_quotas
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
quota := &entity.WorkspaceQuota{}
|
||||
err := r.db.conn.QueryRowContext(ctx, query, id).Scan(
|
||||
"a.ID,
|
||||
"a.WorkspaceID,
|
||||
"a.ResourceType,
|
||||
"a.HardLimit,
|
||||
"a.SoftLimit,
|
||||
"a.Used,
|
||||
"a.CreatedAt,
|
||||
"a.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get quota: %w", err)
|
||||
}
|
||||
|
||||
return quota, nil
|
||||
}
|
||||
|
||||
// GetByWorkspaceAndType 根据 workspace 和资源类型获取配额
|
||||
func (r *QuotaRepository) GetByWorkspaceAndType(ctx context.Context, workspaceID string, resourceType entity.ResourceType) (*entity.WorkspaceQuota, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, resource_type, hard_limit, soft_limit, used, created_at, updated_at
|
||||
FROM workspace_quotas
|
||||
WHERE workspace_id = $1 AND resource_type = $2
|
||||
`
|
||||
|
||||
quota := &entity.WorkspaceQuota{}
|
||||
err := r.db.conn.QueryRowContext(ctx, query, workspaceID, resourceType).Scan(
|
||||
"a.ID,
|
||||
"a.WorkspaceID,
|
||||
"a.ResourceType,
|
||||
"a.HardLimit,
|
||||
"a.SoftLimit,
|
||||
"a.Used,
|
||||
"a.CreatedAt,
|
||||
"a.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get quota: %w", err)
|
||||
}
|
||||
|
||||
return quota, nil
|
||||
}
|
||||
|
||||
// GetByWorkspace 获取 workspace 的所有配额
|
||||
func (r *QuotaRepository) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.WorkspaceQuota, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, resource_type, hard_limit, soft_limit, used, created_at, updated_at
|
||||
FROM workspace_quotas
|
||||
WHERE workspace_id = $1
|
||||
ORDER BY resource_type
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, workspaceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list quotas: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
quotas := make([]*entity.WorkspaceQuota, 0)
|
||||
for rows.Next() {
|
||||
quota := &entity.WorkspaceQuota{}
|
||||
err := rows.Scan(
|
||||
"a.ID,
|
||||
"a.WorkspaceID,
|
||||
"a.ResourceType,
|
||||
"a.HardLimit,
|
||||
"a.SoftLimit,
|
||||
"a.Used,
|
||||
"a.CreatedAt,
|
||||
"a.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan quota: %w", err)
|
||||
}
|
||||
quotas = append(quotas, quota)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
return quotas, nil
|
||||
}
|
||||
|
||||
// Update 更新配额
|
||||
func (r *QuotaRepository) Update(ctx context.Context, quota *entity.WorkspaceQuota) error {
|
||||
quota.UpdatedAt = time.Now()
|
||||
|
||||
query := `
|
||||
UPDATE workspace_quotas
|
||||
SET hard_limit = $1, soft_limit = $2, used = $3, updated_at = $4
|
||||
WHERE id = $5
|
||||
`
|
||||
|
||||
result, err := r.db.conn.ExecContext(ctx, query,
|
||||
quota.HardLimit,
|
||||
quota.SoftLimit,
|
||||
quota.Used,
|
||||
quota.UpdatedAt,
|
||||
quota.ID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update quota: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get affected rows: %w", err)
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("quota not found")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除配额
|
||||
func (r *QuotaRepository) Delete(ctx context.Context, id string) error {
|
||||
query := `DELETE FROM workspace_quotas WHERE id = $1`
|
||||
|
||||
_, err := r.db.conn.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete quota: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteByWorkspace 删除 workspace 的所有配额
|
||||
func (r *QuotaRepository) DeleteByWorkspace(ctx context.Context, workspaceID string) error {
|
||||
query := `DELETE FROM workspace_quotas WHERE workspace_id = $1`
|
||||
|
||||
_, err := r.db.conn.ExecContext(ctx, query, workspaceID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete quotas: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -208,7 +208,7 @@ func (r *RegistryRepository) Delete(ctx context.Context, id string) error {
|
||||
// List 列出所有 Registries
|
||||
func (r *RegistryRepository) List(ctx context.Context) ([]*entity.Registry, error) {
|
||||
query := `
|
||||
SELECT id, name, url, description, username, password, insecure, created_at, updated_at
|
||||
SELECT id, workspace_id, owner_id, name, url, description, username, password, insecure, is_shared, created_at, updated_at
|
||||
FROM registries
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
@ -222,16 +222,19 @@ func (r *RegistryRepository) List(ctx context.Context) ([]*entity.Registry, erro
|
||||
registries := make([]*entity.Registry, 0)
|
||||
for rows.Next() {
|
||||
registry := &entity.Registry{}
|
||||
var encryptedPassword string
|
||||
var encryptedPassword, workspaceID, ownerID sql.NullString
|
||||
|
||||
err := rows.Scan(
|
||||
®istry.ID,
|
||||
&workspaceID,
|
||||
&ownerID,
|
||||
®istry.Name,
|
||||
®istry.URL,
|
||||
®istry.Description,
|
||||
®istry.Username,
|
||||
&encryptedPassword,
|
||||
®istry.Insecure,
|
||||
®istry.IsShared,
|
||||
®istry.CreatedAt,
|
||||
®istry.UpdatedAt,
|
||||
)
|
||||
@ -239,10 +242,13 @@ func (r *RegistryRepository) List(ctx context.Context) ([]*entity.Registry, erro
|
||||
return nil, fmt.Errorf("failed to scan registry: %w", err)
|
||||
}
|
||||
|
||||
// 处理 NULL 值
|
||||
registry.WorkspaceID = workspaceID.String
|
||||
registry.OwnerID = ownerID.String
|
||||
|
||||
// 解密密码
|
||||
registry.Password, err = r.encryptor.Decrypt(encryptedPassword)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt password: %w", err)
|
||||
if encryptedPassword.Valid {
|
||||
registry.Password, _ = r.encryptor.Decrypt(encryptedPassword.String)
|
||||
}
|
||||
|
||||
registries = append(registries, registry)
|
||||
|
||||
@ -0,0 +1,326 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/repository"
|
||||
)
|
||||
|
||||
// StorageRepository PostgreSQL 存储后端仓储实现
|
||||
type StorageRepository struct {
|
||||
db *DB
|
||||
}
|
||||
|
||||
// NewStorageRepository 创建 PostgreSQL 存储后端仓储
|
||||
func NewStorageRepository(db *DB) repository.StorageRepository {
|
||||
return &StorageRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建存储后端
|
||||
func (r *StorageRepository) Create(ctx context.Context, storage *entity.StorageBackend) error {
|
||||
if storage.ID == "" {
|
||||
storage.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
configJSON, err := json.Marshal(storage.Config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal config: %w", err)
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO storage_backends (id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
`
|
||||
|
||||
_, err = r.db.conn.ExecContext(ctx, query,
|
||||
storage.ID,
|
||||
storage.WorkspaceID,
|
||||
storage.OwnerID,
|
||||
storage.Name,
|
||||
storage.Type,
|
||||
configJSON,
|
||||
storage.Description,
|
||||
storage.IsDefault,
|
||||
storage.IsShared,
|
||||
storage.CreatedAt,
|
||||
storage.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create storage: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 根据 ID 获取存储后端
|
||||
func (r *StorageRepository) GetByID(ctx context.Context, id string) (*entity.StorageBackend, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at
|
||||
FROM storage_backends
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
storage := &entity.StorageBackend{}
|
||||
var configJSON []byte
|
||||
err := r.db.conn.QueryRowContext(ctx, query, id).Scan(
|
||||
&storage.ID,
|
||||
&storage.WorkspaceID,
|
||||
&storage.OwnerID,
|
||||
&storage.Name,
|
||||
&storage.Type,
|
||||
&configJSON,
|
||||
&storage.Description,
|
||||
&storage.IsDefault,
|
||||
&storage.IsShared,
|
||||
&storage.CreatedAt,
|
||||
&storage.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, entity.ErrStorageNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get storage: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(configJSON, &storage.Config); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
return storage, nil
|
||||
}
|
||||
|
||||
// GetByName 根据名称获取存储后端
|
||||
func (r *StorageRepository) GetByName(ctx context.Context, workspaceID, name string) (*entity.StorageBackend, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at
|
||||
FROM storage_backends
|
||||
WHERE workspace_id = $1 AND name = $2
|
||||
`
|
||||
|
||||
storage := &entity.StorageBackend{}
|
||||
var configJSON []byte
|
||||
err := r.db.conn.QueryRowContext(ctx, query, workspaceID, name).Scan(
|
||||
&storage.ID,
|
||||
&storage.WorkspaceID,
|
||||
&storage.OwnerID,
|
||||
&storage.Name,
|
||||
&storage.Type,
|
||||
&configJSON,
|
||||
&storage.Description,
|
||||
&storage.IsDefault,
|
||||
&storage.IsShared,
|
||||
&storage.CreatedAt,
|
||||
&storage.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, entity.ErrStorageNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get storage: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(configJSON, &storage.Config); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
return storage, nil
|
||||
}
|
||||
|
||||
// GetByWorkspace 获取 workspace 的所有存储后端
|
||||
func (r *StorageRepository) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.StorageBackend, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at
|
||||
FROM storage_backends
|
||||
WHERE workspace_id = $1 OR is_shared = TRUE
|
||||
ORDER BY is_default DESC, name
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, workspaceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list storage: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanStorages(rows)
|
||||
}
|
||||
|
||||
// GetShared 获取所有共享存储后端
|
||||
func (r *StorageRepository) GetShared(ctx context.Context) ([]*entity.StorageBackend, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at
|
||||
FROM storage_backends
|
||||
WHERE is_shared = TRUE
|
||||
ORDER BY name
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list shared storage: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanStorages(rows)
|
||||
}
|
||||
|
||||
// GetDefault 获取 workspace 的默认存储后端
|
||||
func (r *StorageRepository) GetDefault(ctx context.Context, workspaceID string) (*entity.StorageBackend, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at
|
||||
FROM storage_backends
|
||||
WHERE workspace_id = $1 AND is_default = TRUE
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
storage := &entity.StorageBackend{}
|
||||
var configJSON []byte
|
||||
err := r.db.conn.QueryRowContext(ctx, query, workspaceID).Scan(
|
||||
&storage.ID,
|
||||
&storage.WorkspaceID,
|
||||
&storage.OwnerID,
|
||||
&storage.Name,
|
||||
&storage.Type,
|
||||
&configJSON,
|
||||
&storage.Description,
|
||||
&storage.IsDefault,
|
||||
&storage.IsShared,
|
||||
&storage.CreatedAt,
|
||||
&storage.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get default storage: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(configJSON, &storage.Config); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
return storage, nil
|
||||
}
|
||||
|
||||
// Update 更新存储后端
|
||||
func (r *StorageRepository) Update(ctx context.Context, storage *entity.StorageBackend) error {
|
||||
storage.UpdatedAt = time.Now()
|
||||
|
||||
configJSON, err := json.Marshal(storage.Config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal config: %w", err)
|
||||
}
|
||||
|
||||
query := `
|
||||
UPDATE storage_backends
|
||||
SET name = $1, type = $2, config = $3, description = $4, is_default = $5, is_shared = $6, updated_at = $7
|
||||
WHERE id = $8
|
||||
`
|
||||
|
||||
result, err := r.db.conn.ExecContext(ctx, query,
|
||||
storage.Name,
|
||||
storage.Type,
|
||||
configJSON,
|
||||
storage.Description,
|
||||
storage.IsDefault,
|
||||
storage.IsShared,
|
||||
storage.UpdatedAt,
|
||||
storage.ID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update storage: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get affected rows: %w", err)
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return entity.ErrStorageNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除存储后端
|
||||
func (r *StorageRepository) Delete(ctx context.Context, id string) error {
|
||||
query := `DELETE FROM storage_backends WHERE id = $1`
|
||||
|
||||
result, err := r.db.conn.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete storage: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get affected rows: %w", err)
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return entity.ErrStorageNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// List 列出所有存储后端(管理员用)
|
||||
func (r *StorageRepository) List(ctx context.Context) ([]*entity.StorageBackend, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at
|
||||
FROM storage_backends
|
||||
ORDER BY workspace_id, name
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list storage: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanStorages(rows)
|
||||
}
|
||||
|
||||
// scanStorages 扫描多行结果
|
||||
func (r *StorageRepository) scanStorages(rows *sql.Rows) ([]*entity.StorageBackend, error) {
|
||||
storages := make([]*entity.StorageBackend, 0)
|
||||
for rows.Next() {
|
||||
storage := &entity.StorageBackend{}
|
||||
var configJSON []byte
|
||||
err := rows.Scan(
|
||||
&storage.ID,
|
||||
&storage.WorkspaceID,
|
||||
&storage.OwnerID,
|
||||
&storage.Name,
|
||||
&storage.Type,
|
||||
&configJSON,
|
||||
&storage.Description,
|
||||
&storage.IsDefault,
|
||||
&storage.IsShared,
|
||||
&storage.CreatedAt,
|
||||
&storage.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan storage: %w", err)
|
||||
}
|
||||
if err := json.Unmarshal(configJSON, &storage.Config); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
storages = append(storages, storage)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
return storages, nil
|
||||
}
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@ -27,9 +28,14 @@ func (r *UserRepository) Create(ctx context.Context, user *entity.User) error {
|
||||
user.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
if user.IsActive {
|
||||
user.IsActive = true
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO users (id, username, password_hash, email, revoked_after, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
INSERT INTO users (id, username, password_hash, email, role, workspace_id, is_active, must_change_password, revoked_after, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
`
|
||||
|
||||
_, err := r.db.conn.ExecContext(ctx, query,
|
||||
@ -37,6 +43,10 @@ func (r *UserRepository) Create(ctx context.Context, user *entity.User) error {
|
||||
user.Username,
|
||||
user.PasswordHash,
|
||||
user.Email,
|
||||
user.Role,
|
||||
user.WorkspaceID,
|
||||
user.IsActive,
|
||||
user.MustChangePassword,
|
||||
user.RevokedAfter,
|
||||
user.CreatedAt,
|
||||
user.UpdatedAt,
|
||||
@ -52,22 +62,34 @@ func (r *UserRepository) Create(ctx context.Context, user *entity.User) error {
|
||||
// GetByID 根据 ID 获取用户
|
||||
func (r *UserRepository) GetByID(ctx context.Context, id string) (*entity.User, error) {
|
||||
query := `
|
||||
SELECT id, username, password_hash, email, revoked_after, created_at, updated_at
|
||||
SELECT id, username, password_hash, email, role, workspace_id, is_active, must_change_password, revoked_after, created_at, updated_at
|
||||
FROM users
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
user := &entity.User{}
|
||||
var workspaceID sql.NullString
|
||||
err := r.db.conn.QueryRowContext(ctx, query, id).Scan(
|
||||
&user.ID,
|
||||
&user.Username,
|
||||
&user.PasswordHash,
|
||||
&user.Email,
|
||||
&user.Role,
|
||||
&workspaceID,
|
||||
&user.IsActive,
|
||||
&user.MustChangePassword,
|
||||
&user.RevokedAfter,
|
||||
&user.CreatedAt,
|
||||
&user.UpdatedAt,
|
||||
)
|
||||
|
||||
// Handle NULL workspace_id
|
||||
if workspaceID.Valid {
|
||||
user.WorkspaceID = workspaceID.String
|
||||
} else {
|
||||
user.WorkspaceID = ""
|
||||
}
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, entity.ErrUserNotFound
|
||||
}
|
||||
@ -80,30 +102,50 @@ func (r *UserRepository) GetByID(ctx context.Context, id string) (*entity.User,
|
||||
|
||||
// GetByUsername 根据用户名获取用户
|
||||
func (r *UserRepository) GetByUsername(ctx context.Context, username string) (*entity.User, error) {
|
||||
log.Printf("[DEBUG] GetByUsername called with username: %q", username)
|
||||
query := `
|
||||
SELECT id, username, password_hash, email, revoked_after, created_at, updated_at
|
||||
SELECT id, username, password_hash, email, role, workspace_id, is_active, must_change_password, revoked_after, created_at, updated_at
|
||||
FROM users
|
||||
WHERE username = $1
|
||||
`
|
||||
|
||||
log.Printf("[DEBUG] Executing query: %s with param: %s", query, username)
|
||||
|
||||
user := &entity.User{}
|
||||
var workspaceID sql.NullString
|
||||
err := r.db.conn.QueryRowContext(ctx, query, username).Scan(
|
||||
&user.ID,
|
||||
&user.Username,
|
||||
&user.PasswordHash,
|
||||
&user.Email,
|
||||
&user.Role,
|
||||
&workspaceID,
|
||||
&user.IsActive,
|
||||
&user.MustChangePassword,
|
||||
&user.RevokedAfter,
|
||||
&user.CreatedAt,
|
||||
&user.UpdatedAt,
|
||||
)
|
||||
|
||||
// Handle NULL workspace_id
|
||||
if workspaceID.Valid {
|
||||
user.WorkspaceID = workspaceID.String
|
||||
} else {
|
||||
user.WorkspaceID = ""
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Query result - err: %v", err)
|
||||
if err == sql.ErrNoRows {
|
||||
log.Printf("[DEBUG] User not found in DB")
|
||||
return nil, entity.ErrUserNotFound
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] Scan error: %v", err)
|
||||
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Found user: %+v", user)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
@ -113,14 +155,18 @@ func (r *UserRepository) Update(ctx context.Context, user *entity.User) error {
|
||||
|
||||
query := `
|
||||
UPDATE users
|
||||
SET username = $1, password_hash = $2, email = $3, revoked_after = $4, updated_at = $5
|
||||
WHERE id = $6
|
||||
SET username = $1, password_hash = $2, email = $3, role = $4, workspace_id = $5, is_active = $6, must_change_password = $7, revoked_after = $8, updated_at = $9
|
||||
WHERE id = $10
|
||||
`
|
||||
|
||||
result, err := r.db.conn.ExecContext(ctx, query,
|
||||
user.Username,
|
||||
user.PasswordHash,
|
||||
user.Email,
|
||||
user.Role,
|
||||
user.WorkspaceID,
|
||||
user.IsActive,
|
||||
user.MustChangePassword,
|
||||
user.RevokedAfter,
|
||||
user.UpdatedAt,
|
||||
user.ID,
|
||||
@ -166,7 +212,7 @@ func (r *UserRepository) Delete(ctx context.Context, id string) error {
|
||||
// List 列出所有用户
|
||||
func (r *UserRepository) List(ctx context.Context) ([]*entity.User, error) {
|
||||
query := `
|
||||
SELECT id, username, password_hash, email, revoked_after, created_at, updated_at
|
||||
SELECT id, username, password_hash, email, role, workspace_id, is_active, must_change_password, revoked_after, created_at, updated_at
|
||||
FROM users
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
@ -185,6 +231,98 @@ func (r *UserRepository) List(ctx context.Context) ([]*entity.User, error) {
|
||||
&user.Username,
|
||||
&user.PasswordHash,
|
||||
&user.Email,
|
||||
&user.Role,
|
||||
&user.WorkspaceID,
|
||||
&user.IsActive,
|
||||
&user.MustChangePassword,
|
||||
&user.RevokedAfter,
|
||||
&user.CreatedAt,
|
||||
&user.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan user: %w", err)
|
||||
}
|
||||
users = append(users, user)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// ListByWorkspace 列出指定 workspace 的用户
|
||||
func (r *UserRepository) ListByWorkspace(ctx context.Context, workspaceID string) ([]*entity.User, error) {
|
||||
query := `
|
||||
SELECT id, username, password_hash, email, role, workspace_id, is_active, must_change_password, revoked_after, created_at, updated_at
|
||||
FROM users
|
||||
WHERE workspace_id = $1
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, workspaceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list users by workspace: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
users := make([]*entity.User, 0)
|
||||
for rows.Next() {
|
||||
user := &entity.User{}
|
||||
err := rows.Scan(
|
||||
&user.ID,
|
||||
&user.Username,
|
||||
&user.PasswordHash,
|
||||
&user.Email,
|
||||
&user.Role,
|
||||
&user.WorkspaceID,
|
||||
&user.IsActive,
|
||||
&user.MustChangePassword,
|
||||
&user.RevokedAfter,
|
||||
&user.CreatedAt,
|
||||
&user.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan user: %w", err)
|
||||
}
|
||||
users = append(users, user)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// ListActive 仅列出活跃用户
|
||||
func (r *UserRepository) ListActive(ctx context.Context) ([]*entity.User, error) {
|
||||
query := `
|
||||
SELECT id, username, password_hash, email, role, workspace_id, is_active, must_change_password, revoked_after, created_at, updated_at
|
||||
FROM users
|
||||
WHERE is_active = TRUE
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list active users: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
users := make([]*entity.User, 0)
|
||||
for rows.Next() {
|
||||
user := &entity.User{}
|
||||
err := rows.Scan(
|
||||
&user.ID,
|
||||
&user.Username,
|
||||
&user.PasswordHash,
|
||||
&user.Email,
|
||||
&user.Role,
|
||||
&user.WorkspaceID,
|
||||
&user.IsActive,
|
||||
&user.MustChangePassword,
|
||||
&user.RevokedAfter,
|
||||
&user.CreatedAt,
|
||||
&user.UpdatedAt,
|
||||
|
||||
@ -0,0 +1,287 @@
|
||||
package postgres
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// ValuesTemplateRepository PostgreSQL Values 模板仓储实现
|
||||
type ValuesTemplateRepository struct {
|
||||
db *DB
|
||||
}
|
||||
|
||||
// NewValuesTemplateRepository 创建 PostgreSQL Values 模板仓储
|
||||
func NewValuesTemplateRepository(db *DB) repository.ValuesTemplateRepository {
|
||||
return &ValuesTemplateRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建 Values 模板
|
||||
func (r *ValuesTemplateRepository) Create(ctx context.Context, template *entity.ValuesTemplate) error {
|
||||
if template.ID == "" {
|
||||
template.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO values_templates (id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
`
|
||||
|
||||
_, err := r.db.conn.ExecContext(ctx, query,
|
||||
template.ID,
|
||||
template.WorkspaceID,
|
||||
template.OwnerID,
|
||||
template.ChartReferenceID,
|
||||
template.Name,
|
||||
template.Description,
|
||||
template.ValuesYAML,
|
||||
template.Version,
|
||||
template.IsDefault,
|
||||
template.CreatedAt,
|
||||
template.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create values template: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 根据 ID 获取 Values 模板
|
||||
func (r *ValuesTemplateRepository) GetByID(ctx context.Context, id string) (*entity.ValuesTemplate, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at
|
||||
FROM values_templates
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
template := &entity.ValuesTemplate{}
|
||||
err := r.db.conn.QueryRowContext(ctx, query, id).Scan(
|
||||
&template.ID,
|
||||
&template.WorkspaceID,
|
||||
&template.OwnerID,
|
||||
&template.ChartReferenceID,
|
||||
&template.Name,
|
||||
&template.Description,
|
||||
&template.ValuesYAML,
|
||||
&template.Version,
|
||||
&template.IsDefault,
|
||||
&template.CreatedAt,
|
||||
&template.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, entity.ErrTemplateNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get values template: %w", err)
|
||||
}
|
||||
|
||||
return template, nil
|
||||
}
|
||||
|
||||
// GetByWorkspace 获取 workspace 的所有 Values 模板
|
||||
func (r *ValuesTemplateRepository) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.ValuesTemplate, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at
|
||||
FROM values_templates
|
||||
WHERE workspace_id = $1
|
||||
ORDER BY chart_reference_id, name, version DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, workspaceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list values templates: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanValuesTemplates(rows)
|
||||
}
|
||||
|
||||
// GetByChartReference 获取 Chart Reference 的所有 Values 模板
|
||||
func (r *ValuesTemplateRepository) GetByChartReference(ctx context.Context, chartRefID string) ([]*entity.ValuesTemplate, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at
|
||||
FROM values_templates
|
||||
WHERE chart_reference_id = $1
|
||||
ORDER BY name, version DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, chartRefID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list values templates: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanValuesTemplates(rows)
|
||||
}
|
||||
|
||||
// GetByName 根据名称获取 Values 模板(获取最新版本)
|
||||
func (r *ValuesTemplateRepository) GetByName(ctx context.Context, workspaceID, chartRefID, name string) (*entity.ValuesTemplate, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at
|
||||
FROM values_templates
|
||||
WHERE workspace_id = $1 AND chart_reference_id = $2 AND name = $3
|
||||
ORDER BY version DESC
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
template := &entity.ValuesTemplate{}
|
||||
err := r.db.conn.QueryRowContext(ctx, query, workspaceID, chartRefID, name).Scan(
|
||||
&template.ID,
|
||||
&template.WorkspaceID,
|
||||
&template.OwnerID,
|
||||
&template.ChartReferenceID,
|
||||
&template.Name,
|
||||
&template.Description,
|
||||
&template.ValuesYAML,
|
||||
&template.Version,
|
||||
&template.IsDefault,
|
||||
&template.CreatedAt,
|
||||
&template.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, entity.ErrTemplateNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get values template: %w", err)
|
||||
}
|
||||
|
||||
return template, nil
|
||||
}
|
||||
|
||||
// GetHistory 获取模板的版本历史
|
||||
func (r *ValuesTemplateRepository) GetHistory(ctx context.Context, chartRefID, name string) ([]*entity.ValuesTemplate, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at
|
||||
FROM values_templates
|
||||
WHERE chart_reference_id = $1 AND name = $2
|
||||
ORDER BY version DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, chartRefID, name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get values template history: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanValuesTemplates(rows)
|
||||
}
|
||||
|
||||
// Update 更新 Values 模板(自动递增版本)
|
||||
func (r *ValuesTemplateRepository) Update(ctx context.Context, template *entity.ValuesTemplate) error {
|
||||
// 获取当前最大版本号
|
||||
var maxVersion int
|
||||
err := r.db.conn.QueryRowContext(ctx,
|
||||
"SELECT COALESCE(MAX(version), 0) FROM values_templates WHERE chart_reference_id = $1 AND name = $2",
|
||||
template.ChartReferenceID, template.Name,
|
||||
).Scan(&maxVersion)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get max version: %w", err)
|
||||
}
|
||||
|
||||
// 生成新 ID 用于新版本
|
||||
newID := uuid.New().String()
|
||||
template.Version = maxVersion + 1
|
||||
template.UpdatedAt = time.Now()
|
||||
template.CreatedAt = time.Now() // 新版本的创建时间
|
||||
|
||||
query := `
|
||||
INSERT INTO values_templates (id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
`
|
||||
|
||||
_, err = r.db.conn.ExecContext(ctx, query,
|
||||
newID,
|
||||
template.WorkspaceID,
|
||||
template.OwnerID,
|
||||
template.ChartReferenceID,
|
||||
template.Name,
|
||||
template.Description,
|
||||
template.ValuesYAML,
|
||||
template.Version,
|
||||
template.IsDefault,
|
||||
template.CreatedAt,
|
||||
template.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update values template: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除 Values 模板
|
||||
func (r *ValuesTemplateRepository) Delete(ctx context.Context, id string) error {
|
||||
// 获取模板信息
|
||||
template, err := r.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除该名称的所有版本
|
||||
query := `DELETE FROM values_templates WHERE chart_reference_id = $1 AND name = $2`
|
||||
_, err = r.db.conn.ExecContext(ctx, query, template.ChartReferenceID, template.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete values template: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// List 列出所有 Values 模板(管理员用)
|
||||
func (r *ValuesTemplateRepository) List(ctx context.Context) ([]*entity.ValuesTemplate, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at
|
||||
FROM values_templates
|
||||
ORDER BY workspace_id, chart_reference_id, name, version DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list values templates: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanValuesTemplates(rows)
|
||||
}
|
||||
|
||||
// scanValuesTemplates 扫描多行结果
|
||||
func (r *ValuesTemplateRepository) scanValuesTemplates(rows *sql.Rows) ([]*entity.ValuesTemplate, error) {
|
||||
templates := make([]*entity.ValuesTemplate, 0)
|
||||
for rows.Next() {
|
||||
template := &entity.ValuesTemplate{}
|
||||
err := rows.Scan(
|
||||
&template.ID,
|
||||
&template.WorkspaceID,
|
||||
&template.OwnerID,
|
||||
&template.ChartReferenceID,
|
||||
&template.Name,
|
||||
&template.Description,
|
||||
&template.ValuesYAML,
|
||||
&template.Version,
|
||||
&template.IsDefault,
|
||||
&template.CreatedAt,
|
||||
&template.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan values template: %w", err)
|
||||
}
|
||||
templates = append(templates, template)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
return templates, nil
|
||||
}
|
||||
@ -0,0 +1,197 @@
|
||||
package postgres
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// WorkspaceRepository PostgreSQL Workspace 仓储实现
|
||||
type WorkspaceRepository struct {
|
||||
db *DB
|
||||
}
|
||||
|
||||
// NewWorkspaceRepository 创建 PostgreSQL Workspace 仓储
|
||||
func NewWorkspaceRepository(db *DB) repository.WorkspaceRepository {
|
||||
return &WorkspaceRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建 Workspace
|
||||
func (r *WorkspaceRepository) Create(ctx context.Context, workspace *entity.Workspace) error {
|
||||
if workspace.ID == "" {
|
||||
workspace.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO workspaces (id, name, description, created_by, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
`
|
||||
|
||||
_, err := r.db.conn.ExecContext(ctx, query,
|
||||
workspace.ID,
|
||||
workspace.Name,
|
||||
workspace.Description,
|
||||
workspace.CreatedBy,
|
||||
workspace.CreatedAt,
|
||||
workspace.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create workspace: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 根据 ID 获取 Workspace
|
||||
func (r *WorkspaceRepository) GetByID(ctx context.Context, id string) (*entity.Workspace, error) {
|
||||
query := `
|
||||
SELECT id, name, description, created_by, created_at, updated_at
|
||||
FROM workspaces
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
workspace := &entity.Workspace{}
|
||||
err := r.db.conn.QueryRowContext(ctx, query, id).Scan(
|
||||
&workspace.ID,
|
||||
&workspace.Name,
|
||||
&workspace.Description,
|
||||
&workspace.CreatedBy,
|
||||
&workspace.CreatedAt,
|
||||
&workspace.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, entity.ErrWorkspaceNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get workspace: %w", err)
|
||||
}
|
||||
|
||||
return workspace, nil
|
||||
}
|
||||
|
||||
// GetByName 根据名称获取 Workspace
|
||||
func (r *WorkspaceRepository) GetByName(ctx context.Context, name string) (*entity.Workspace, error) {
|
||||
query := `
|
||||
SELECT id, name, description, created_by, created_at, updated_at
|
||||
FROM workspaces
|
||||
WHERE name = $1
|
||||
`
|
||||
|
||||
workspace := &entity.Workspace{}
|
||||
err := r.db.conn.QueryRowContext(ctx, query, name).Scan(
|
||||
&workspace.ID,
|
||||
&workspace.Name,
|
||||
&workspace.Description,
|
||||
&workspace.CreatedBy,
|
||||
&workspace.CreatedAt,
|
||||
&workspace.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, entity.ErrWorkspaceNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get workspace: %w", err)
|
||||
}
|
||||
|
||||
return workspace, nil
|
||||
}
|
||||
|
||||
// Update 更新 Workspace
|
||||
func (r *WorkspaceRepository) Update(ctx context.Context, workspace *entity.Workspace) error {
|
||||
workspace.UpdatedAt = time.Now()
|
||||
|
||||
query := `
|
||||
UPDATE workspaces
|
||||
SET name = $1, description = $2, updated_at = $3
|
||||
WHERE id = $4
|
||||
`
|
||||
|
||||
result, err := r.db.conn.ExecContext(ctx, query,
|
||||
workspace.Name,
|
||||
workspace.Description,
|
||||
workspace.UpdatedAt,
|
||||
workspace.ID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update workspace: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get affected rows: %w", err)
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return entity.ErrWorkspaceNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除 Workspace
|
||||
func (r *WorkspaceRepository) Delete(ctx context.Context, id string) error {
|
||||
query := `DELETE FROM workspaces WHERE id = $1`
|
||||
|
||||
result, err := r.db.conn.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete workspace: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get affected rows: %w", err)
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return entity.ErrWorkspaceNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// List 列出所有 Workspace
|
||||
func (r *WorkspaceRepository) List(ctx context.Context) ([]*entity.Workspace, error) {
|
||||
query := `
|
||||
SELECT id, name, description, created_by, created_at, updated_at
|
||||
FROM workspaces
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list workspaces: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
workspaces := make([]*entity.Workspace, 0)
|
||||
for rows.Next() {
|
||||
workspace := &entity.Workspace{}
|
||||
err := rows.Scan(
|
||||
&workspace.ID,
|
||||
&workspace.Name,
|
||||
&workspace.Description,
|
||||
&workspace.CreatedBy,
|
||||
&workspace.CreatedAt,
|
||||
&workspace.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan workspace: %w", err)
|
||||
}
|
||||
workspaces = append(workspaces, workspace)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
return workspaces, nil
|
||||
}
|
||||
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)
|
||||
}
|
||||
@ -26,98 +26,106 @@ func NewJWTManager(secretKey string) *JWTManager {
|
||||
|
||||
// Claims JWT Claims
|
||||
type Claims struct {
|
||||
UserID string `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
UserID string `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
WorkspaceID string `json:"workspace_id"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// Generate 生成 Access Token 和 Refresh Token
|
||||
func (m *JWTManager) Generate(userID, username string) (accessToken, refreshToken string, err error) {
|
||||
func (m *JWTManager) Generate(userID, username, role, workspaceID string) (accessToken, refreshToken string, err error) {
|
||||
// 生成 Access Token
|
||||
accessClaims := &Claims{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
Role: role,
|
||||
WorkspaceID: workspaceID,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(AccessTokenDuration)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
accessTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
|
||||
accessToken, err = accessTokenObj.SignedString([]byte(m.secretKey))
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to sign access token: %w", err)
|
||||
}
|
||||
|
||||
|
||||
// 生成 Refresh Token
|
||||
refreshClaims := &Claims{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
Role: role,
|
||||
WorkspaceID: workspaceID,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(RefreshTokenDuration)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
refreshTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
|
||||
refreshToken, err = refreshTokenObj.SignedString([]byte(m.secretKey))
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to sign refresh token: %w", err)
|
||||
}
|
||||
|
||||
|
||||
return accessToken, refreshToken, nil
|
||||
}
|
||||
|
||||
// Verify 验证 Token
|
||||
func (m *JWTManager) Verify(tokenString string) (userID, username string, err error) {
|
||||
userID, username, _, err = m.VerifyWithIssuedAt(tokenString)
|
||||
return userID, username, err
|
||||
func (m *JWTManager) Verify(tokenString string) (userID, username, role, workspaceID string, err error) {
|
||||
userID, username, role, workspaceID, _, err = m.VerifyWithIssuedAt(tokenString)
|
||||
return userID, username, role, workspaceID, err
|
||||
}
|
||||
|
||||
// VerifyWithIssuedAt 验证 Token 并返回签发时间
|
||||
func (m *JWTManager) VerifyWithIssuedAt(tokenString string) (userID, username string, issuedAt int64, err error) {
|
||||
func (m *JWTManager) VerifyWithIssuedAt(tokenString string) (userID, username, role, workspaceID string, issuedAt int64, err error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(m.secretKey), nil
|
||||
})
|
||||
|
||||
|
||||
if err != nil {
|
||||
return "", "", 0, fmt.Errorf("failed to parse token: %w", err)
|
||||
return "", "", "", "", 0, fmt.Errorf("failed to parse token: %w", err)
|
||||
}
|
||||
|
||||
|
||||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||
return claims.UserID, claims.Username, claims.IssuedAt.Unix(), nil
|
||||
return claims.UserID, claims.Username, claims.Role, claims.WorkspaceID, claims.IssuedAt.Unix(), nil
|
||||
}
|
||||
|
||||
return "", "", 0, fmt.Errorf("invalid token")
|
||||
|
||||
return "", "", "", "", 0, fmt.Errorf("invalid token")
|
||||
}
|
||||
|
||||
// Refresh 刷新 Token
|
||||
func (m *JWTManager) Refresh(refreshToken string) (string, error) {
|
||||
// 验证 Refresh Token
|
||||
userID, username, err := m.Verify(refreshToken)
|
||||
userID, username, role, workspaceID, err := m.Verify(refreshToken)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid refresh token: %w", err)
|
||||
}
|
||||
|
||||
|
||||
// 生成新的 Access Token
|
||||
accessClaims := &Claims{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
Role: role,
|
||||
WorkspaceID: workspaceID,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(AccessTokenDuration)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
accessTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
|
||||
newAccessToken, err := accessTokenObj.SignedString([]byte(m.secretKey))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign new access token: %w", err)
|
||||
}
|
||||
|
||||
|
||||
return newAccessToken, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user