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
|
||||
}
|
||||
Reference in New Issue
Block a user