feat(frontend): add Helm chart browser, monitoring, chart-references and values templates pages

Add new frontend pages for the multi-tenant OCDP platform:

- Charts page (/charts): Browse Harbor OCI registries to list Helm chart repositories
  and versions, with deploy modal to launch charts on selected clusters
- Monitoring page (/monitoring): Display cluster metrics (CPU/Memory/GPU usage)
  and per-node details with resource utilization bars
- Chart References page (/chart-references): CRUD for chart metadata references
- Values Templates page (/templates): CRUD for Helm values templates with version
  history and rollback support
- Sidebar: Add Charts navigation, update Storage and Templates links
- api.ts: Add all API client functions (clusterApi, registryApi, instanceApi,
  monitoringApi, storageApi, chartReferenceApi, valuesTemplateApi,
  workspaceApi, userApi) with full TypeScript types

Note: deploy flow and values template rollback not yet end-to-end tested.
This commit is contained in:
Ivan087
2026-04-15 16:59:31 +08:00
parent c5e51ed069
commit 29d0310f03
283 changed files with 24658 additions and 36038 deletions

View File

@ -42,3 +42,8 @@ type ValuesSchemaResponse struct {
Schema string `json:"schema"`
}
// ValuesResponse Values 响应
type ValuesResponse struct {
Values string `json:"values"`
}

View File

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

View File

@ -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"`

View File

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

View File

@ -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"`

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

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

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