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