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
}

View File

@ -127,6 +127,69 @@ func (f *AdapterFactory) CreateEntryClient() repository.InstanceEntryClient {
return k8s.NewEntryClient()
}
// CreateWorkspaceRepository 创建 Workspace 仓储
func (f *AdapterFactory) CreateWorkspaceRepository() (repository.WorkspaceRepository, error) {
if f.mode == ModeMock {
return nil, fmt.Errorf("workspace repository mock not implemented")
}
// 默认真实实现PostgreSQL
if err := f.ensureDBConnection(); err != nil {
return nil, err
}
return postgres.NewWorkspaceRepository(f.db), nil
}
// CreateQuotaRepository 创建 Quota 仓储
// CreateStorageRepository 创建存储后端仓储
func (f *AdapterFactory) CreateStorageRepository() (repository.StorageRepository, error) {
if f.mode == ModeMock {
return mock.NewStorageRepositoryMock(), nil
}
if err := f.ensureDBConnection(); err != nil {
return nil, err
}
return postgres.NewStorageRepository(f.db), nil
}
// CreateQuotaRepository 创建配额仓储
func (f *AdapterFactory) CreateQuotaRepository() (repository.QuotaRepository, error) {
if f.mode == ModeMock {
return nil, fmt.Errorf("quota repository mock not implemented")
}
// 默认真实实现PostgreSQL
if err := f.ensureDBConnection(); err != nil {
return nil, err
}
return postgres.NewQuotaRepository(f.db), nil
}
// CreateChartReferenceRepository 创建 Chart 引用仓储
func (f *AdapterFactory) CreateChartReferenceRepository() (repository.ChartReferenceRepository, error) {
if f.mode == ModeMock {
return nil, fmt.Errorf("chart reference repository mock not implemented")
}
if err := f.ensureDBConnection(); err != nil {
return nil, err
}
return postgres.NewChartReferenceRepository(f.db), nil
}
// CreateValuesTemplateRepository 创建 Values 模板仓储
func (f *AdapterFactory) CreateValuesTemplateRepository() (repository.ValuesTemplateRepository, error) {
if f.mode == ModeMock {
return nil, fmt.Errorf("values template repository mock not implemented")
}
if err := f.ensureDBConnection(); err != nil {
return nil, err
}
return postgres.NewValuesTemplateRepository(f.db), nil
}
// CreateAllRepositories 一次性创建所有 Repositories
func (f *AdapterFactory) CreateAllRepositories() (*Repositories, error) {
userRepo, err := f.CreateUserRepository()
@ -149,6 +212,21 @@ func (f *AdapterFactory) CreateAllRepositories() (*Repositories, error) {
return nil, fmt.Errorf("failed to create instance repository: %w", err)
}
workspaceRepo, err := f.CreateWorkspaceRepository()
if err != nil {
return nil, fmt.Errorf("failed to create workspace repository: %w", err)
}
storageRepo, err := f.CreateStorageRepository()
if err != nil {
return nil, fmt.Errorf("failed to create storage repository: %w", err)
}
quotaRepo, err := f.CreateQuotaRepository()
if err != nil {
return nil, fmt.Errorf("failed to create quota repository: %w", err)
}
ociClient, err := f.CreateOCIClient()
if err != nil {
return nil, fmt.Errorf("failed to create OCI client: %w", err)
@ -163,28 +241,48 @@ func (f *AdapterFactory) CreateAllRepositories() (*Repositories, error) {
metricsClient := f.CreateMetricsClient(clusterRepo)
entryClient := f.CreateEntryClient()
chartRefRepo, err := f.CreateChartReferenceRepository()
if err != nil {
return nil, fmt.Errorf("failed to create chart reference repository: %w", err)
}
valuesTemplateRepo, err := f.CreateValuesTemplateRepository()
if err != nil {
return nil, fmt.Errorf("failed to create values template repository: %w", err)
}
return &Repositories{
UserRepo: userRepo,
ClusterRepo: clusterRepo,
RegistryRepo: registryRepo,
InstanceRepo: instanceRepo,
OCIClient: ociClient,
HelmClient: helmClient,
MetricsClient: metricsClient,
EntryClient: entryClient,
UserRepo: userRepo,
ClusterRepo: clusterRepo,
RegistryRepo: registryRepo,
InstanceRepo: instanceRepo,
WorkspaceRepo: workspaceRepo,
StorageRepo: storageRepo,
ChartRefRepo: chartRefRepo,
ValuesTemplateRepo: valuesTemplateRepo,
QuotaRepo: quotaRepo,
OCIClient: ociClient,
HelmClient: helmClient,
MetricsClient: metricsClient,
EntryClient: entryClient,
}, nil
}
// Repositories 所有仓储的集合
type Repositories struct {
UserRepo repository.UserRepository
ClusterRepo repository.ClusterRepository
RegistryRepo repository.RegistryRepository
InstanceRepo repository.InstanceRepository
OCIClient repository.OCIClient
HelmClient repository.HelmClient
MetricsClient repository.MetricsClient
EntryClient repository.InstanceEntryClient
UserRepo repository.UserRepository
ClusterRepo repository.ClusterRepository
RegistryRepo repository.RegistryRepository
InstanceRepo repository.InstanceRepository
WorkspaceRepo repository.WorkspaceRepository
StorageRepo repository.StorageRepository
ChartRefRepo repository.ChartReferenceRepository
ValuesTemplateRepo repository.ValuesTemplateRepository
QuotaRepo repository.QuotaRepository
OCIClient repository.OCIClient
HelmClient repository.HelmClient
MetricsClient repository.MetricsClient
EntryClient repository.InstanceEntryClient
}
// ensureDBConnection 确保数据库连接已建立

View File

@ -262,12 +262,40 @@ func (c *OCIClientMock) GetValuesSchema(ctx context.Context, registry *entity.Re
return mockSchema, nil
}
func (c *OCIClientMock) GetValues(ctx context.Context, registry *entity.Registry, repository, reference string) (string, error) {
artifact, err := c.GetArtifact(ctx, registry, repository, reference)
if err != nil {
return "", err
}
if !artifact.IsChart() {
return "", fmt.Errorf("not a helm chart")
}
// 返回 Mock values.yaml
mockValues := `# Default values for the chart
replicaCount: 1
image:
repository: nginx
tag: latest
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
resources: {}
`
return mockValues, nil
}
func (c *OCIClientMock) PullArtifact(ctx context.Context, registry *entity.Registry, repository, reference, destPath string) error {
_, err := c.GetArtifact(ctx, registry, repository, reference)
if err != nil {
return err
}
// Mock 实现,不实际下载
return nil
}

View File

@ -43,13 +43,26 @@ func (c *OCIClient) getRegistry(reg *entity.Registry) (*remote.Registry, error)
return nil, fmt.Errorf("failed to create registry client: %w", err)
}
// 设置认证
if reg.Username != "" && reg.Password != "" {
// 设置认证 - 优先使用 registry 自己的凭证,否则使用 .env 中的默认凭证
username := reg.Username
password := reg.Password
// 如果没有提供凭证,尝试从环境变量加载
if (username == "" || password == "") && strings.Contains(reg.URL, "harbor") {
if envUser := os.Getenv("HARBOR_USERNAME"); envUser != "" {
username = envUser
}
if envPass := os.Getenv("HARBOR_PASSWORD"); envPass != "" {
password = envPass
}
}
if username != "" && password != "" {
registry.Client = &auth.Client{
Client: c.httpClient,
Credential: auth.StaticCredential(registryURL, auth.Credential{
Username: reg.Username,
Password: reg.Password,
Username: username,
Password: password,
}),
}
}
@ -370,6 +383,105 @@ func (c *OCIClient) GetValuesSchema(ctx context.Context, registry *entity.Regist
return "", entity.ErrValuesSchemaNotFound
}
// GetValues 获取 Helm Chart 的 values.yaml
func (c *OCIClient) GetValues(ctx context.Context, registry *entity.Registry, repository, reference string) (string, error) {
reg, err := c.getRegistry(registry)
if err != nil {
return "", err
}
repo, err := reg.Repository(ctx, repository)
if err != nil {
return "", fmt.Errorf("failed to get repository: %w", err)
}
// 解析 reference (tag 或 digest)
desc, err := repo.Resolve(ctx, reference)
if err != nil {
return "", fmt.Errorf("failed to resolve artifact: %w", err)
}
manifestReader, err := repo.Fetch(ctx, desc)
if err != nil {
return "", fmt.Errorf("failed to fetch manifest: %w", err)
}
defer manifestReader.Close()
manifestBytes, err := io.ReadAll(manifestReader)
if err != nil {
return "", fmt.Errorf("failed to read manifest: %w", err)
}
var manifest ocispec.Manifest
if err := json.Unmarshal(manifestBytes, &manifest); err != nil {
return "", fmt.Errorf("failed to unmarshal manifest: %w", err)
}
// 查找 Helm Chart layertar+gzip 包含 chart 内容)并从中读取 values.yaml
var chartLayer *ocispec.Descriptor
for i := range manifest.Layers {
layer := manifest.Layers[i]
if strings.Contains(layer.MediaType, "cncf.helm.chart") ||
strings.Contains(layer.MediaType, "helm.chart.content") {
chartLayer = &manifest.Layers[i]
break
}
}
if chartLayer == nil {
return "", entity.ErrValuesNotFound
}
if chartLayer.Digest == "" {
return "", fmt.Errorf("chart layer digest is empty")
}
if _, err := digest.Parse(string(chartLayer.Digest)); err != nil {
return "", fmt.Errorf("invalid chart layer digest: %w", err)
}
layerReader, err := repo.Fetch(ctx, *chartLayer)
if err != nil {
return "", fmt.Errorf("failed to fetch chart layer: %w", err)
}
defer layerReader.Close()
gzipReader, err := gzip.NewReader(layerReader)
if err != nil {
return "", fmt.Errorf("failed to create gzip reader: %w", err)
}
defer gzipReader.Close()
tarReader := tar.NewReader(gzipReader)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return "", fmt.Errorf("failed to read chart archive: %w", err)
}
if header.Typeflag != tar.TypeReg {
continue
}
// 查找 values.yaml 文件(可能在 chart 根目录或子目录中)
// 通常路径格式为: {chart-name}/values.yaml
if strings.HasSuffix(header.Name, "values.yaml") {
data, err := io.ReadAll(tarReader)
if err != nil {
return "", fmt.Errorf("failed to read values.yaml: %w", err)
}
if len(data) == 0 {
return "", entity.ErrValuesNotFound
}
return string(data), nil
}
}
return "", entity.ErrValuesNotFound
}
// PullArtifact 下载 artifact 到本地
func (c *OCIClient) PullArtifact(ctx context.Context, registry *entity.Registry, repository, reference, destPath string) error {
reg, err := c.getRegistry(registry)

View File

@ -104,13 +104,41 @@ func (r *ClusterRepositoryMock) Delete(ctx context.Context, id string) error {
func (r *ClusterRepositoryMock) List(ctx context.Context) ([]*entity.Cluster, error) {
r.mu.RLock()
defer r.mu.RUnlock()
clusters := make([]*entity.Cluster, 0, len(r.clusters))
for _, cluster := range r.clusters {
// 解密敏感数据后返回
clusters = append(clusters, r.decryptCluster(cluster))
}
return clusters, nil
}
func (r *ClusterRepositoryMock) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.Cluster, error) {
r.mu.RLock()
defer r.mu.RUnlock()
clusters := make([]*entity.Cluster, 0)
for _, cluster := range r.clusters {
if cluster.WorkspaceID == workspaceID {
clusters = append(clusters, r.decryptCluster(cluster))
}
}
return clusters, nil
}
func (r *ClusterRepositoryMock) GetShared(ctx context.Context) ([]*entity.Cluster, error) {
r.mu.RLock()
defer r.mu.RUnlock()
clusters := make([]*entity.Cluster, 0)
for _, cluster := range r.clusters {
if cluster.IsShared {
clusters = append(clusters, r.decryptCluster(cluster))
}
}
return clusters, nil
}

View File

@ -102,12 +102,26 @@ func (r *InstanceRepositoryMock) ListByCluster(ctx context.Context, clusterID st
func (r *InstanceRepositoryMock) List(ctx context.Context) ([]*entity.Instance, error) {
r.mu.RLock()
defer r.mu.RUnlock()
instances := make([]*entity.Instance, 0, len(r.instances))
for _, instance := range r.instances {
instances = append(instances, instance)
}
return instances, nil
}
func (r *InstanceRepositoryMock) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.Instance, error) {
r.mu.RLock()
defer r.mu.RUnlock()
instances := make([]*entity.Instance, 0)
for _, instance := range r.instances {
if instance.WorkspaceID == workspaceID {
instances = append(instances, instance)
}
}
return instances, nil
}

View File

@ -0,0 +1,96 @@
package mock
import (
"context"
"github.com/ocdp/cluster-service/internal/domain/entity"
)
// StorageRepositoryMock Storage 仓储 Mock
type StorageRepositoryMock struct {
storages map[string]*entity.StorageBackend
}
// NewStorageRepositoryMock 创建 Mock
func NewStorageRepositoryMock() *StorageRepositoryMock {
return &StorageRepositoryMock{
storages: make(map[string]*entity.StorageBackend),
}
}
// Create 创建存储
func (r *StorageRepositoryMock) Create(ctx context.Context, storage *entity.StorageBackend) error {
r.storages[storage.ID] = storage
return nil
}
// GetByID 获取存储
func (r *StorageRepositoryMock) GetByID(ctx context.Context, id string) (*entity.StorageBackend, error) {
if s, ok := r.storages[id]; ok {
return s, nil
}
return nil, entity.ErrStorageNotFound
}
// GetByWorkspace 获取工作空间的存储
func (r *StorageRepositoryMock) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.StorageBackend, error) {
var result []*entity.StorageBackend
for _, s := range r.storages {
if s.WorkspaceID == workspaceID {
result = append(result, s)
}
}
return result, nil
}
// GetByName 按名称获取
func (r *StorageRepositoryMock) GetByName(ctx context.Context, workspaceID, name string) (*entity.StorageBackend, error) {
for _, s := range r.storages {
if s.WorkspaceID == workspaceID && s.Name == name {
return s, nil
}
}
return nil, entity.ErrStorageNotFound
}
// Update 更新存储
func (r *StorageRepositoryMock) Update(ctx context.Context, storage *entity.StorageBackend) error {
r.storages[storage.ID] = storage
return nil
}
// Delete 删除存储
func (r *StorageRepositoryMock) Delete(ctx context.Context, id string) error {
delete(r.storages, id)
return nil
}
// GetShared 获取所有共享存储后端
func (r *StorageRepositoryMock) GetShared(ctx context.Context) ([]*entity.StorageBackend, error) {
var result []*entity.StorageBackend
for _, s := range r.storages {
if s.IsShared {
result = append(result, s)
}
}
return result, nil
}
// GetDefault 获取 workspace 的默认存储后端
func (r *StorageRepositoryMock) GetDefault(ctx context.Context, workspaceID string) (*entity.StorageBackend, error) {
for _, s := range r.storages {
if s.WorkspaceID == workspaceID && s.IsDefault {
return s, nil
}
}
return nil, entity.ErrStorageNotFound
}
// List 列出所有存储(管理员用)
func (r *StorageRepositoryMock) List(ctx context.Context) ([]*entity.StorageBackend, error) {
var result []*entity.StorageBackend
for _, s := range r.storages {
result = append(result, s)
}
return result, nil
}

View File

@ -88,12 +88,40 @@ func (r *UserRepositoryMock) Delete(ctx context.Context, id string) error {
func (r *UserRepositoryMock) List(ctx context.Context) ([]*entity.User, error) {
r.mu.RLock()
defer r.mu.RUnlock()
users := make([]*entity.User, 0, len(r.users))
for _, user := range r.users {
users = append(users, user)
}
return users, nil
}
func (r *UserRepositoryMock) ListByWorkspace(ctx context.Context, workspaceID string) ([]*entity.User, error) {
r.mu.RLock()
defer r.mu.RUnlock()
users := make([]*entity.User, 0)
for _, user := range r.users {
if user.WorkspaceID == workspaceID {
users = append(users, user)
}
}
return users, nil
}
func (r *UserRepositoryMock) ListActive(ctx context.Context) ([]*entity.User, error) {
r.mu.RLock()
defer r.mu.RUnlock()
users := make([]*entity.User, 0)
for _, user := range r.users {
if user.IsActive {
users = append(users, user)
}
}
return users, nil
}

View File

@ -0,0 +1,200 @@
package postgres
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"github.com/google/uuid"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
// AuditLogRepository PostgreSQL 审计日志仓储实现
type AuditLogRepository struct {
db *DB
}
// NewAuditLogRepository 创建 PostgreSQL 审计日志仓储
func NewAuditLogRepository(db *DB) repository.AuditLogRepository {
return &AuditLogRepository{db: db}
}
// Create 创建审计日志
func (r *AuditLogRepository) Create(ctx context.Context, log *entity.AuditLog) error {
if log.ID == "" {
log.ID = uuid.New().String()
}
detailsJSON, err := json.Marshal(log.Details)
if err != nil {
return fmt.Errorf("failed to marshal details: %w", err)
}
query := `
INSERT INTO audit_logs (id, workspace_id, user_id, action, resource_type, resource_id, resource_name, details, ip_address, user_agent, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
`
_, err = r.db.conn.ExecContext(ctx, query,
log.ID,
log.WorkspaceID,
log.UserID,
log.Action,
log.ResourceType,
log.ResourceID,
log.ResourceName,
detailsJSON,
log.IPAddress,
log.UserAgent,
log.CreatedAt,
)
if err != nil {
return fmt.Errorf("failed to create audit log: %w", err)
}
return nil
}
// GetByWorkspace 获取 workspace 的审计日志
func (r *AuditLogRepository) GetByWorkspace(ctx context.Context, workspaceID string, limit int) ([]*entity.AuditLog, error) {
if limit <= 0 {
limit = 100
}
query := `
SELECT id, workspace_id, user_id, action, resource_type, resource_id, resource_name, details, ip_address, user_agent, created_at
FROM audit_logs
WHERE workspace_id = $1
ORDER BY created_at DESC
LIMIT $2
`
rows, err := r.db.conn.QueryContext(ctx, query, workspaceID, limit)
if err != nil {
return nil, fmt.Errorf("failed to get audit logs: %w", err)
}
defer rows.Close()
return r.scanAuditLogs(rows)
}
// GetByUser 获取用户的审计日志
func (r *AuditLogRepository) GetByUser(ctx context.Context, userID string, limit int) ([]*entity.AuditLog, error) {
if limit <= 0 {
limit = 100
}
query := `
SELECT id, workspace_id, user_id, action, resource_type, resource_id, resource_name, details, ip_address, user_agent, created_at
FROM audit_logs
WHERE user_id = $1
ORDER BY created_at DESC
LIMIT $2
`
rows, err := r.db.conn.QueryContext(ctx, query, userID, limit)
if err != nil {
return nil, fmt.Errorf("failed to get audit logs: %w", err)
}
defer rows.Close()
return r.scanAuditLogs(rows)
}
// GetByResource 获取资源的审计日志
func (r *AuditLogRepository) GetByResource(ctx context.Context, resourceType entity.AuditResourceType, resourceID string, limit int) ([]*entity.AuditLog, error) {
if limit <= 0 {
limit = 100
}
query := `
SELECT id, workspace_id, user_id, admin action, resource_type, resource_id, resource_name, details, ip_address, user_agent, created_at
FROM audit_logs
WHERE resource_type = $1 AND resource_id = $2
ORDER BY created_at DESC
LIMIT $3
`
rows, err := r.db.conn.QueryContext(ctx, query, resourceType, resourceID, limit)
if err != nil {
return nil, fmt.Errorf("failed to get audit logs: %w", err)
}
defer rows.Close()
return r.scanAuditLogs(rows)
}
// List 列出审计日志(分页)
func (r *AuditLogRepository) List(ctx context.Context, limit, offset int) ([]*entity.AuditLog, error) {
if limit <= 0 {
limit = 100
}
if offset < 0 {
offset = 0
}
query := `
SELECT id, workspace_id, user_id, action, resource_type, resource_id, resource_name, details, ip_address, user_agent, created_at
FROM audit_logs
ORDER BY created_at DESC
LIMIT $1 OFFSET $2
`
rows, err := r.db.conn.QueryContext(ctx, query, limit, offset)
if err != nil {
return nil, fmt.Errorf("failed to list audit logs: %w", err)
}
defer rows.Close()
return r.scanAuditLogs(rows)
}
// DeleteByWorkspace 删除 workspace 的审计日志
func (r *AuditLogRepository) DeleteByWorkspace(ctx context.Context, workspaceID string) error {
query := `DELETE FROM audit_logs WHERE workspace_id = $1`
_, err := r.db.conn.ExecContext(ctx, query, workspaceID)
if err != nil {
return fmt.Errorf("failed to delete audit logs: %w", err)
}
return nil
}
// scanAuditLogs 扫描多行结果
func (r *AuditLogRepository) scanAuditLogs(rows *sql.Rows) ([]*entity.AuditLog, error) {
logs := make([]*entity.AuditLog, 0)
for rows.Next() {
log := &entity.AuditLog{}
var detailsJSON []byte
err := rows.Scan(
&log.ID,
&log.WorkspaceID,
&log.UserID,
&log.Action,
&log.ResourceType,
&log.ResourceID,
&log.ResourceName,
&detailsJSON,
&log.IPAddress,
&log.UserAgent,
&log.CreatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan audit log: %w", err)
}
if err := json.Unmarshal(detailsJSON, &log.Details); err != nil {
log.Details = make(map[string]interface{})
}
logs = append(logs, log)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
return logs, nil
}

View File

@ -0,0 +1,253 @@
package postgres
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/google/uuid"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
// ChartReferenceRepository PostgreSQL Chart 引用仓储实现
type ChartReferenceRepository struct {
db *DB
}
// NewChartReferenceRepository 创建 PostgreSQL Chart 引用仓储
func NewChartReferenceRepository(db *DB) repository.ChartReferenceRepository {
return &ChartReferenceRepository{db: db}
}
// Create 创建 Chart 引用
func (r *ChartReferenceRepository) Create(ctx context.Context, chartRef *entity.ChartReference) error {
if chartRef.ID == "" {
chartRef.ID = uuid.New().String()
}
query := `
INSERT INTO chart_references (id, workspace_id, registry_id, repository, chart_name, description, is_enabled, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
`
_, err := r.db.conn.ExecContext(ctx, query,
chartRef.ID,
chartRef.WorkspaceID,
chartRef.RegistryID,
chartRef.Repository,
chartRef.ChartName,
chartRef.Description,
chartRef.IsEnabled,
chartRef.CreatedAt,
chartRef.UpdatedAt,
)
if err != nil {
return fmt.Errorf("failed to create chart reference: %w", err)
}
return nil
}
// GetByID 根据 ID 获取 Chart 引用
func (r *ChartReferenceRepository) GetByID(ctx context.Context, id string) (*entity.ChartReference, error) {
query := `
SELECT id, workspace_id, registry_id, repository, chart_name, description, is_enabled, created_at, updated_at
FROM chart_references
WHERE id = $1
`
chartRef := &entity.ChartReference{}
err := r.db.conn.QueryRowContext(ctx, query, id).Scan(
&chartRef.ID,
&chartRef.WorkspaceID,
&chartRef.RegistryID,
&chartRef.Repository,
&chartRef.ChartName,
&chartRef.Description,
&chartRef.IsEnabled,
&chartRef.CreatedAt,
&chartRef.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, entity.ErrChartReferenceNotFound
}
if err != nil {
return nil, fmt.Errorf("failed to get chart reference: %w", err)
}
return chartRef, nil
}
// GetByWorkspace 获取 workspace 的所有 Chart 引用
func (r *ChartReferenceRepository) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.ChartReference, error) {
query := `
SELECT id, workspace_id, registry_id, repository, chart_name, description, is_enabled, created_at, updated_at
FROM chart_references
WHERE workspace_id = $1
ORDER BY chart_name
`
rows, err := r.db.conn.QueryContext(ctx, query, workspaceID)
if err != nil {
return nil, fmt.Errorf("failed to list chart references: %w", err)
}
defer rows.Close()
return r.scanChartReferences(rows)
}
// GetByRegistry 获取 registry 的所有 Chart 引用
func (r *ChartReferenceRepository) GetByRegistry(ctx context.Context, registryID string) ([]*entity.ChartReference, error) {
query := `
SELECT id, workspace_id, registry_id, repository, chart_name, description, is_enabled, created_at, updated_at
FROM chart_references
WHERE registry_id = $1
ORDER BY chart_name
`
rows, err := r.db.conn.QueryContext(ctx, query, registryID)
if err != nil {
return nil, fmt.Errorf("failed to list chart references: %w", err)
}
defer rows.Close()
return r.scanChartReferences(rows)
}
// GetByName 根据名称获取 Chart 引用
func (r *ChartReferenceRepository) GetByName(ctx context.Context, workspaceID, chartName string) (*entity.ChartReference, error) {
query := `
SELECT id, workspace_id, registry_id, repository, chart_name, description, is_enabled, created_at, updated_at
FROM chart_references
WHERE workspace_id = $1 AND chart_name = $2
`
chartRef := &entity.ChartReference{}
err := r.db.conn.QueryRowContext(ctx, query, workspaceID, chartName).Scan(
&chartRef.ID,
&chartRef.WorkspaceID,
&chartRef.RegistryID,
&chartRef.Repository,
&chartRef.ChartName,
&chartRef.Description,
&chartRef.IsEnabled,
&chartRef.CreatedAt,
&chartRef.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, entity.ErrChartReferenceNotFound
}
if err != nil {
return nil, fmt.Errorf("failed to get chart reference: %w", err)
}
return chartRef, nil
}
// Update 更新 Chart 引用
func (r *ChartReferenceRepository) Update(ctx context.Context, chartRef *entity.ChartReference) error {
chartRef.UpdatedAt = time.Now()
query := `
UPDATE chart_references
SET registry_id = $1, repository = $2, chart_name = $3, description = $4, is_enabled = $5, updated_at = $6
WHERE id = $7
`
result, err := r.db.conn.ExecContext(ctx, query,
chartRef.RegistryID,
chartRef.Repository,
chartRef.ChartName,
chartRef.Description,
chartRef.IsEnabled,
chartRef.UpdatedAt,
chartRef.ID,
)
if err != nil {
return fmt.Errorf("failed to update chart reference: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rows == 0 {
return entity.ErrChartReferenceNotFound
}
return nil
}
// Delete 删除 Chart 引用
func (r *ChartReferenceRepository) Delete(ctx context.Context, id string) error {
query := `DELETE FROM chart_references WHERE id = $1`
result, err := r.db.conn.ExecContext(ctx, query, id)
if err != nil {
return fmt.Errorf("failed to delete chart reference: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rows == 0 {
return entity.ErrChartReferenceNotFound
}
return nil
}
// List 列出所有 Chart 引用(管理员用)
func (r *ChartReferenceRepository) List(ctx context.Context) ([]*entity.ChartReference, error) {
query := `
SELECT id, workspace_id, registry_id, repository, chart_name, description, is_enabled, created_at, updated_at
FROM chart_references
ORDER BY workspace_id, chart_name
`
rows, err := r.db.conn.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to list chart references: %w", err)
}
defer rows.Close()
return r.scanChartReferences(rows)
}
// scanChartReferences 扫描多行结果
func (r *ChartReferenceRepository) scanChartReferences(rows *sql.Rows) ([]*entity.ChartReference, error) {
chartRefs := make([]*entity.ChartReference, 0)
for rows.Next() {
chartRef := &entity.ChartReference{}
err := rows.Scan(
&chartRef.ID,
&chartRef.WorkspaceID,
&chartRef.RegistryID,
&chartRef.Repository,
&chartRef.ChartName,
&chartRef.Description,
&chartRef.IsEnabled,
&chartRef.CreatedAt,
&chartRef.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan chart reference: %w", err)
}
chartRefs = append(chartRefs, chartRef)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
return chartRefs, nil
}

View File

@ -32,6 +32,11 @@ func (r *ClusterRepository) Create(ctx context.Context, cluster *entity.Cluster)
cluster.ID = uuid.New().String()
}
// 设置默认值
if cluster.IsolationMode == "" {
cluster.IsolationMode = entity.IsolationModeNamespace
}
// 加密敏感数据
encryptedCAData, err := r.encryptor.Encrypt(cluster.CAData)
if err != nil {
@ -54,12 +59,14 @@ func (r *ClusterRepository) Create(ctx context.Context, cluster *entity.Cluster)
}
query := `
INSERT INTO clusters (id, name, host, ca_data, cert_data, key_data, token, description, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
INSERT INTO clusters (id, workspace_id, owner_id, name, host, ca_data, cert_data, key_data, token, description, isolation_mode, default_namespace, is_shared, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
`
_, err = r.db.conn.ExecContext(ctx, query,
cluster.ID,
cluster.WorkspaceID,
cluster.OwnerID,
cluster.Name,
cluster.Host,
encryptedCAData,
@ -67,6 +74,9 @@ func (r *ClusterRepository) Create(ctx context.Context, cluster *entity.Cluster)
encryptedKeyData,
encryptedToken,
cluster.Description,
cluster.IsolationMode,
cluster.DefaultNamespace,
cluster.IsShared,
cluster.CreatedAt,
cluster.UpdatedAt,
)
@ -81,7 +91,7 @@ func (r *ClusterRepository) Create(ctx context.Context, cluster *entity.Cluster)
// GetByID 根据 ID 获取集群
func (r *ClusterRepository) GetByID(ctx context.Context, id string) (*entity.Cluster, error) {
query := `
SELECT id, name, host, ca_data, cert_data, key_data, token, description, created_at, updated_at
SELECT id, workspace_id, owner_id, name, host, ca_data, cert_data, key_data, token, description, isolation_mode, default_namespace, is_shared, created_at, updated_at
FROM clusters
WHERE id = $1
`
@ -91,6 +101,8 @@ func (r *ClusterRepository) GetByID(ctx context.Context, id string) (*entity.Clu
err := r.db.conn.QueryRowContext(ctx, query, id).Scan(
&cluster.ID,
&cluster.WorkspaceID,
&cluster.OwnerID,
&cluster.Name,
&cluster.Host,
&encryptedCAData,
@ -98,6 +110,9 @@ func (r *ClusterRepository) GetByID(ctx context.Context, id string) (*entity.Clu
&encryptedKeyData,
&encryptedToken,
&cluster.Description,
&cluster.IsolationMode,
&cluster.DefaultNamespace,
&cluster.IsShared,
&cluster.CreatedAt,
&cluster.UpdatedAt,
)
@ -110,25 +125,10 @@ func (r *ClusterRepository) GetByID(ctx context.Context, id string) (*entity.Clu
}
// 解密敏感数据
cluster.CAData, err = r.encryptor.Decrypt(encryptedCAData)
if err != nil {
return nil, fmt.Errorf("failed to decrypt CA data: %w", err)
}
cluster.CertData, err = r.encryptor.Decrypt(encryptedCertData)
if err != nil {
return nil, fmt.Errorf("failed to decrypt cert data: %w", err)
}
cluster.KeyData, err = r.encryptor.Decrypt(encryptedKeyData)
if err != nil {
return nil, fmt.Errorf("failed to decrypt key data: %w", err)
}
cluster.Token, err = r.encryptor.Decrypt(encryptedToken)
if err != nil {
return nil, fmt.Errorf("failed to decrypt token: %w", err)
}
cluster.CAData, _ = r.encryptor.Decrypt(encryptedCAData)
cluster.CertData, _ = r.encryptor.Decrypt(encryptedCertData)
cluster.KeyData, _ = r.encryptor.Decrypt(encryptedKeyData)
cluster.Token, _ = r.encryptor.Decrypt(encryptedToken)
return cluster, nil
}
@ -136,7 +136,7 @@ func (r *ClusterRepository) GetByID(ctx context.Context, id string) (*entity.Clu
// GetByName 根据名称获取集群
func (r *ClusterRepository) GetByName(ctx context.Context, name string) (*entity.Cluster, error) {
query := `
SELECT id, name, host, ca_data, cert_data, key_data, token, description, created_at, updated_at
SELECT id, workspace_id, owner_id, name, host, ca_data, cert_data, key_data, token, description, isolation_mode, default_namespace, is_shared, created_at, updated_at
FROM clusters
WHERE name = $1
`
@ -146,6 +146,8 @@ func (r *ClusterRepository) GetByName(ctx context.Context, name string) (*entity
err := r.db.conn.QueryRowContext(ctx, query, name).Scan(
&cluster.ID,
&cluster.WorkspaceID,
&cluster.OwnerID,
&cluster.Name,
&cluster.Host,
&encryptedCAData,
@ -153,6 +155,9 @@ func (r *ClusterRepository) GetByName(ctx context.Context, name string) (*entity
&encryptedKeyData,
&encryptedToken,
&cluster.Description,
&cluster.IsolationMode,
&cluster.DefaultNamespace,
&cluster.IsShared,
&cluster.CreatedAt,
&cluster.UpdatedAt,
)
@ -165,25 +170,10 @@ func (r *ClusterRepository) GetByName(ctx context.Context, name string) (*entity
}
// 解密敏感数据
cluster.CAData, err = r.encryptor.Decrypt(encryptedCAData)
if err != nil {
return nil, fmt.Errorf("failed to decrypt CA data: %w", err)
}
cluster.CertData, err = r.encryptor.Decrypt(encryptedCertData)
if err != nil {
return nil, fmt.Errorf("failed to decrypt cert data: %w", err)
}
cluster.KeyData, err = r.encryptor.Decrypt(encryptedKeyData)
if err != nil {
return nil, fmt.Errorf("failed to decrypt key data: %w", err)
}
cluster.Token, err = r.encryptor.Decrypt(encryptedToken)
if err != nil {
return nil, fmt.Errorf("failed to decrypt token: %w", err)
}
cluster.CAData, _ = r.encryptor.Decrypt(encryptedCAData)
cluster.CertData, _ = r.encryptor.Decrypt(encryptedCertData)
cluster.KeyData, _ = r.encryptor.Decrypt(encryptedKeyData)
cluster.Token, _ = r.encryptor.Decrypt(encryptedToken)
return cluster, nil
}
@ -215,9 +205,10 @@ func (r *ClusterRepository) Update(ctx context.Context, cluster *entity.Cluster)
query := `
UPDATE clusters
SET name = $1, host = $2, ca_data = $3, cert_data = $4, key_data = $5,
token = $6, description = $7, updated_at = $8
WHERE id = $9
SET name = $1, host = $2, ca_data = $3, cert_data = $4, key_data = $5,
token = $6, description = $7, isolation_mode = $8, default_namespace = $9,
is_shared = $10, updated_at = $11
WHERE id = $12
`
result, err := r.db.conn.ExecContext(ctx, query,
@ -228,6 +219,9 @@ func (r *ClusterRepository) Update(ctx context.Context, cluster *entity.Cluster)
encryptedKeyData,
encryptedToken,
cluster.Description,
cluster.IsolationMode,
cluster.DefaultNamespace,
cluster.IsShared,
cluster.UpdatedAt,
cluster.ID,
)
@ -272,7 +266,7 @@ func (r *ClusterRepository) Delete(ctx context.Context, id string) error {
// List 列出所有集群
func (r *ClusterRepository) List(ctx context.Context) ([]*entity.Cluster, error) {
query := `
SELECT id, name, host, ca_data, cert_data, key_data, token, description, created_at, updated_at
SELECT id, workspace_id, owner_id, name, host, ca_data, cert_data, key_data, token, description, isolation_mode, default_namespace, is_shared, created_at, updated_at
FROM clusters
ORDER BY created_at DESC
`
@ -283,13 +277,59 @@ func (r *ClusterRepository) List(ctx context.Context) ([]*entity.Cluster, error)
}
defer rows.Close()
return r.scanClusters(rows)
}
// GetByWorkspace 获取 workspace 的所有集群(包括共享集群)
func (r *ClusterRepository) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.Cluster, error) {
query := `
SELECT id, workspace_id, owner_id, name, host, ca_data, cert_data, key_data, token, description, isolation_mode, default_namespace, is_shared, created_at, updated_at
FROM clusters
WHERE workspace_id = $1 OR is_shared = TRUE
ORDER BY is_shared, created_at DESC
`
rows, err := r.db.conn.QueryContext(ctx, query, workspaceID)
if err != nil {
return nil, fmt.Errorf("failed to list clusters by workspace: %w", err)
}
defer rows.Close()
return r.scanClusters(rows)
}
// GetShared 获取所有共享集群
func (r *ClusterRepository) GetShared(ctx context.Context) ([]*entity.Cluster, error) {
query := `
SELECT id, workspace_id, owner_id, name, host, ca_data, cert_data, key_data, token, description, isolation_mode, default_namespace, is_shared, created_at, updated_at
FROM clusters
WHERE is_shared = TRUE
ORDER BY created_at DESC
`
rows, err := r.db.conn.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to list shared clusters: %w", err)
}
defer rows.Close()
return r.scanClusters(rows)
}
// scanClusters 扫描多行结果
func (r *ClusterRepository) scanClusters(rows *sql.Rows) ([]*entity.Cluster, error) {
clusters := make([]*entity.Cluster, 0)
for rows.Next() {
cluster := &entity.Cluster{}
var encryptedCAData, encryptedCertData, encryptedKeyData, encryptedToken string
var (
encryptedCAData, encryptedCertData, encryptedKeyData, encryptedToken sql.NullString
workspaceID, ownerID, defaultNamespace sql.NullString
)
err := rows.Scan(
&cluster.ID,
&workspaceID,
&ownerID,
&cluster.Name,
&cluster.Host,
&encryptedCAData,
@ -297,6 +337,9 @@ func (r *ClusterRepository) List(ctx context.Context) ([]*entity.Cluster, error)
&encryptedKeyData,
&encryptedToken,
&cluster.Description,
&cluster.IsolationMode,
&defaultNamespace,
&cluster.IsShared,
&cluster.CreatedAt,
&cluster.UpdatedAt,
)
@ -304,25 +347,23 @@ func (r *ClusterRepository) List(ctx context.Context) ([]*entity.Cluster, error)
return nil, fmt.Errorf("failed to scan cluster: %w", err)
}
// 处理 NULL 值
cluster.WorkspaceID = workspaceID.String
cluster.OwnerID = ownerID.String
cluster.DefaultNamespace = defaultNamespace.String
// 解密敏感数据
cluster.CAData, err = r.encryptor.Decrypt(encryptedCAData)
if err != nil {
return nil, fmt.Errorf("failed to decrypt CA data: %w", err)
if encryptedCAData.Valid {
cluster.CAData, _ = r.encryptor.Decrypt(encryptedCAData.String)
}
cluster.CertData, err = r.encryptor.Decrypt(encryptedCertData)
if err != nil {
return nil, fmt.Errorf("failed to decrypt cert data: %w", err)
if encryptedCertData.Valid {
cluster.CertData, _ = r.encryptor.Decrypt(encryptedCertData.String)
}
cluster.KeyData, err = r.encryptor.Decrypt(encryptedKeyData)
if err != nil {
return nil, fmt.Errorf("failed to decrypt key data: %w", err)
if encryptedKeyData.Valid {
cluster.KeyData, _ = r.encryptor.Decrypt(encryptedKeyData.String)
}
cluster.Token, err = r.encryptor.Decrypt(encryptedToken)
if err != nil {
return nil, fmt.Errorf("failed to decrypt token: %w", err)
if encryptedToken.Valid {
cluster.Token, _ = r.encryptor.Decrypt(encryptedToken.String)
}
clusters = append(clusters, cluster)

View File

@ -124,6 +124,58 @@ func (db *DB) InitSchema() error {
CREATE INDEX IF NOT EXISTS idx_instances_cluster ON instances(cluster_id);
CREATE INDEX IF NOT EXISTS idx_instances_registry ON instances(registry_id);
CREATE INDEX IF NOT EXISTS idx_instances_name ON instances(name);
-- Storage Backends 表
CREATE TABLE IF NOT EXISTS storage_backends (
id VARCHAR(36) PRIMARY KEY,
workspace_id VARCHAR(36),
owner_id VARCHAR(36),
name VARCHAR(255) NOT NULL,
type VARCHAR(50) NOT NULL,
config JSONB NOT NULL,
description TEXT,
is_default BOOLEAN DEFAULT FALSE,
is_shared BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_storage_workspace ON storage_backends(workspace_id);
-- Chart References 表
CREATE TABLE IF NOT EXISTS chart_references (
id VARCHAR(36) PRIMARY KEY,
workspace_id VARCHAR(36),
registry_id VARCHAR(36),
repository VARCHAR(500) NOT NULL,
chart_name VARCHAR(255) NOT NULL,
description TEXT,
is_enabled BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_chart_workspace ON chart_references(workspace_id);
CREATE INDEX IF NOT EXISTS idx_chart_registry ON chart_references(registry_id);
-- Values Templates 表 - 使用复合唯一键替代主键,允许同一模板的多个版本
CREATE TABLE IF NOT EXISTS values_templates (
id VARCHAR(36),
workspace_id VARCHAR(36),
owner_id VARCHAR(36),
chart_reference_id VARCHAR(36),
name VARCHAR(255) NOT NULL,
description TEXT,
values_yaml TEXT NOT NULL,
version INTEGER NOT NULL DEFAULT 1,
is_default BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (chart_reference_id, name, version)
);
CREATE INDEX IF NOT EXISTS idx_values_template_chart ON values_templates(chart_reference_id);
CREATE INDEX IF NOT EXISTS idx_values_template_workspace ON values_templates(workspace_id);
`
_, err := db.conn.Exec(schema)

View File

@ -431,3 +431,105 @@ func (r *InstanceRepository) List(ctx context.Context) ([]*entity.Instance, erro
return instances, nil
}
// GetByWorkspace 列出指定工作空间的所有实例(用于配额检查)
func (r *InstanceRepository) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.Instance, error) {
query := `
SELECT id, cluster_id, workspace_id, owner_id, name, namespace, registry_id, repository, chart, version,
description, values, values_yaml, values_template_id, user_override_yaml,
status, status_reason, last_operation, last_error, revision,
cpu_requested, memory_requested, gpu_requested, gpu_memory_requested,
created_at, updated_at
FROM instances
WHERE workspace_id = $1
ORDER BY created_at DESC
`
rows, err := r.db.conn.QueryContext(ctx, query, workspaceID)
if err != nil {
return nil, fmt.Errorf("failed to get instances by workspace: %w", err)
}
defer rows.Close()
instances := make([]*entity.Instance, 0)
for rows.Next() {
instance := &entity.Instance{}
var (
valuesJSON []byte
statusReason sql.NullString
lastOperation sql.NullString
lastError sql.NullString
valuesTemplateID sql.NullString
userOverrideYAML sql.NullString
memoryRequested sql.NullString
gpuMemoryRequested sql.NullString
)
err := rows.Scan(
&instance.ID,
&instance.ClusterID,
&instance.WorkspaceID,
&instance.OwnerID,
&instance.Name,
&instance.Namespace,
&instance.RegistryID,
&instance.Repository,
&instance.Chart,
&instance.Version,
&instance.Description,
&valuesJSON,
&instance.ValuesYAML,
&valuesTemplateID,
&userOverrideYAML,
&instance.Status,
&statusReason,
&lastOperation,
&lastError,
&instance.Revision,
&instance.CPURequested,
&memoryRequested,
&instance.GPURequested,
&gpuMemoryRequested,
&instance.CreatedAt,
&instance.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan instance: %w", err)
}
if valuesJSON != nil {
if err := json.Unmarshal(valuesJSON, &instance.Values); err != nil {
return nil, fmt.Errorf("failed to unmarshal values: %w", err)
}
}
if valuesTemplateID.Valid {
instance.ValuesTemplateID = valuesTemplateID.String
}
if userOverrideYAML.Valid {
instance.UserOverrideYAML = userOverrideYAML.String
}
if statusReason.Valid {
instance.StatusReason = statusReason.String
}
if lastOperation.Valid {
instance.LastOperation = entity.InstanceOperation(lastOperation.String)
}
if lastError.Valid {
instance.LastError = lastError.String
}
if memoryRequested.Valid {
instance.MemoryRequested = memoryRequested.String
}
if gpuMemoryRequested.Valid {
instance.GPUMemoryRequested = gpuMemoryRequested.String
}
instances = append(instances, instance)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
return instances, nil
}

View File

@ -0,0 +1,212 @@
package postgres
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/google/uuid"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
// QuotaRepository PostgreSQL 配额仓储实现
type QuotaRepository struct {
db *DB
}
// NewQuotaRepository 创建 PostgreSQL 配额仓储
func NewQuotaRepository(db *DB) repository.QuotaRepository {
return &QuotaRepository{db: db}
}
// Create 创建配额
func (r *QuotaRepository) Create(ctx context.Context, quota *entity.WorkspaceQuota) error {
if quota.ID == "" {
quota.ID = uuid.New().String()
}
query := `
INSERT INTO workspace_quotas (id, workspace_id, resource_type, hard_limit, soft_limit, used, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (workspace_id, resource_type) DO UPDATE
SET hard_limit = $4, soft_limit = $5, updated_at = $8
`
_, err := r.db.conn.ExecContext(ctx, query,
quota.ID,
quota.WorkspaceID,
quota.ResourceType,
quota.HardLimit,
quota.SoftLimit,
quota.Used,
quota.CreatedAt,
quota.UpdatedAt,
)
if err != nil {
return fmt.Errorf("failed to create quota: %w", err)
}
return nil
}
// GetByID 根据 ID 获取配额
func (r *QuotaRepository) GetByID(ctx context.Context, id string) (*entity.WorkspaceQuota, error) {
query := `
SELECT id, workspace_id, resource_type, hard_limit, soft_limit, used, created_at, updated_at
FROM workspace_quotas
WHERE id = $1
`
quota := &entity.WorkspaceQuota{}
err := r.db.conn.QueryRowContext(ctx, query, id).Scan(
&quota.ID,
&quota.WorkspaceID,
&quota.ResourceType,
&quota.HardLimit,
&quota.SoftLimit,
&quota.Used,
&quota.CreatedAt,
&quota.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("failed to get quota: %w", err)
}
return quota, nil
}
// GetByWorkspaceAndType 根据 workspace 和资源类型获取配额
func (r *QuotaRepository) GetByWorkspaceAndType(ctx context.Context, workspaceID string, resourceType entity.ResourceType) (*entity.WorkspaceQuota, error) {
query := `
SELECT id, workspace_id, resource_type, hard_limit, soft_limit, used, created_at, updated_at
FROM workspace_quotas
WHERE workspace_id = $1 AND resource_type = $2
`
quota := &entity.WorkspaceQuota{}
err := r.db.conn.QueryRowContext(ctx, query, workspaceID, resourceType).Scan(
&quota.ID,
&quota.WorkspaceID,
&quota.ResourceType,
&quota.HardLimit,
&quota.SoftLimit,
&quota.Used,
&quota.CreatedAt,
&quota.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("failed to get quota: %w", err)
}
return quota, nil
}
// GetByWorkspace 获取 workspace 的所有配额
func (r *QuotaRepository) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.WorkspaceQuota, error) {
query := `
SELECT id, workspace_id, resource_type, hard_limit, soft_limit, used, created_at, updated_at
FROM workspace_quotas
WHERE workspace_id = $1
ORDER BY resource_type
`
rows, err := r.db.conn.QueryContext(ctx, query, workspaceID)
if err != nil {
return nil, fmt.Errorf("failed to list quotas: %w", err)
}
defer rows.Close()
quotas := make([]*entity.WorkspaceQuota, 0)
for rows.Next() {
quota := &entity.WorkspaceQuota{}
err := rows.Scan(
&quota.ID,
&quota.WorkspaceID,
&quota.ResourceType,
&quota.HardLimit,
&quota.SoftLimit,
&quota.Used,
&quota.CreatedAt,
&quota.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan quota: %w", err)
}
quotas = append(quotas, quota)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
return quotas, nil
}
// Update 更新配额
func (r *QuotaRepository) Update(ctx context.Context, quota *entity.WorkspaceQuota) error {
quota.UpdatedAt = time.Now()
query := `
UPDATE workspace_quotas
SET hard_limit = $1, soft_limit = $2, used = $3, updated_at = $4
WHERE id = $5
`
result, err := r.db.conn.ExecContext(ctx, query,
quota.HardLimit,
quota.SoftLimit,
quota.Used,
quota.UpdatedAt,
quota.ID,
)
if err != nil {
return fmt.Errorf("failed to update quota: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rows == 0 {
return fmt.Errorf("quota not found")
}
return nil
}
// Delete 删除配额
func (r *QuotaRepository) Delete(ctx context.Context, id string) error {
query := `DELETE FROM workspace_quotas WHERE id = $1`
_, err := r.db.conn.ExecContext(ctx, query, id)
if err != nil {
return fmt.Errorf("failed to delete quota: %w", err)
}
return nil
}
// DeleteByWorkspace 删除 workspace 的所有配额
func (r *QuotaRepository) DeleteByWorkspace(ctx context.Context, workspaceID string) error {
query := `DELETE FROM workspace_quotas WHERE workspace_id = $1`
_, err := r.db.conn.ExecContext(ctx, query, workspaceID)
if err != nil {
return fmt.Errorf("failed to delete quotas: %w", err)
}
return nil
}

View File

@ -208,7 +208,7 @@ func (r *RegistryRepository) Delete(ctx context.Context, id string) error {
// List 列出所有 Registries
func (r *RegistryRepository) List(ctx context.Context) ([]*entity.Registry, error) {
query := `
SELECT id, name, url, description, username, password, insecure, created_at, updated_at
SELECT id, workspace_id, owner_id, name, url, description, username, password, insecure, is_shared, created_at, updated_at
FROM registries
ORDER BY created_at DESC
`
@ -222,16 +222,19 @@ func (r *RegistryRepository) List(ctx context.Context) ([]*entity.Registry, erro
registries := make([]*entity.Registry, 0)
for rows.Next() {
registry := &entity.Registry{}
var encryptedPassword string
var encryptedPassword, workspaceID, ownerID sql.NullString
err := rows.Scan(
&registry.ID,
&workspaceID,
&ownerID,
&registry.Name,
&registry.URL,
&registry.Description,
&registry.Username,
&encryptedPassword,
&registry.Insecure,
&registry.IsShared,
&registry.CreatedAt,
&registry.UpdatedAt,
)
@ -239,10 +242,13 @@ func (r *RegistryRepository) List(ctx context.Context) ([]*entity.Registry, erro
return nil, fmt.Errorf("failed to scan registry: %w", err)
}
// 处理 NULL 值
registry.WorkspaceID = workspaceID.String
registry.OwnerID = ownerID.String
// 解密密码
registry.Password, err = r.encryptor.Decrypt(encryptedPassword)
if err != nil {
return nil, fmt.Errorf("failed to decrypt password: %w", err)
if encryptedPassword.Valid {
registry.Password, _ = r.encryptor.Decrypt(encryptedPassword.String)
}
registries = append(registries, registry)

View File

@ -0,0 +1,326 @@
package postgres
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"time"
"github.com/google/uuid"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
// StorageRepository PostgreSQL 存储后端仓储实现
type StorageRepository struct {
db *DB
}
// NewStorageRepository 创建 PostgreSQL 存储后端仓储
func NewStorageRepository(db *DB) repository.StorageRepository {
return &StorageRepository{db: db}
}
// Create 创建存储后端
func (r *StorageRepository) Create(ctx context.Context, storage *entity.StorageBackend) error {
if storage.ID == "" {
storage.ID = uuid.New().String()
}
configJSON, err := json.Marshal(storage.Config)
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
query := `
INSERT INTO storage_backends (id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
`
_, err = r.db.conn.ExecContext(ctx, query,
storage.ID,
storage.WorkspaceID,
storage.OwnerID,
storage.Name,
storage.Type,
configJSON,
storage.Description,
storage.IsDefault,
storage.IsShared,
storage.CreatedAt,
storage.UpdatedAt,
)
if err != nil {
return fmt.Errorf("failed to create storage: %w", err)
}
return nil
}
// GetByID 根据 ID 获取存储后端
func (r *StorageRepository) GetByID(ctx context.Context, id string) (*entity.StorageBackend, error) {
query := `
SELECT id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at
FROM storage_backends
WHERE id = $1
`
storage := &entity.StorageBackend{}
var configJSON []byte
err := r.db.conn.QueryRowContext(ctx, query, id).Scan(
&storage.ID,
&storage.WorkspaceID,
&storage.OwnerID,
&storage.Name,
&storage.Type,
&configJSON,
&storage.Description,
&storage.IsDefault,
&storage.IsShared,
&storage.CreatedAt,
&storage.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, entity.ErrStorageNotFound
}
if err != nil {
return nil, fmt.Errorf("failed to get storage: %w", err)
}
if err := json.Unmarshal(configJSON, &storage.Config); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
return storage, nil
}
// GetByName 根据名称获取存储后端
func (r *StorageRepository) GetByName(ctx context.Context, workspaceID, name string) (*entity.StorageBackend, error) {
query := `
SELECT id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at
FROM storage_backends
WHERE workspace_id = $1 AND name = $2
`
storage := &entity.StorageBackend{}
var configJSON []byte
err := r.db.conn.QueryRowContext(ctx, query, workspaceID, name).Scan(
&storage.ID,
&storage.WorkspaceID,
&storage.OwnerID,
&storage.Name,
&storage.Type,
&configJSON,
&storage.Description,
&storage.IsDefault,
&storage.IsShared,
&storage.CreatedAt,
&storage.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, entity.ErrStorageNotFound
}
if err != nil {
return nil, fmt.Errorf("failed to get storage: %w", err)
}
if err := json.Unmarshal(configJSON, &storage.Config); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
return storage, nil
}
// GetByWorkspace 获取 workspace 的所有存储后端
func (r *StorageRepository) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.StorageBackend, error) {
query := `
SELECT id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at
FROM storage_backends
WHERE workspace_id = $1 OR is_shared = TRUE
ORDER BY is_default DESC, name
`
rows, err := r.db.conn.QueryContext(ctx, query, workspaceID)
if err != nil {
return nil, fmt.Errorf("failed to list storage: %w", err)
}
defer rows.Close()
return r.scanStorages(rows)
}
// GetShared 获取所有共享存储后端
func (r *StorageRepository) GetShared(ctx context.Context) ([]*entity.StorageBackend, error) {
query := `
SELECT id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at
FROM storage_backends
WHERE is_shared = TRUE
ORDER BY name
`
rows, err := r.db.conn.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to list shared storage: %w", err)
}
defer rows.Close()
return r.scanStorages(rows)
}
// GetDefault 获取 workspace 的默认存储后端
func (r *StorageRepository) GetDefault(ctx context.Context, workspaceID string) (*entity.StorageBackend, error) {
query := `
SELECT id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at
FROM storage_backends
WHERE workspace_id = $1 AND is_default = TRUE
LIMIT 1
`
storage := &entity.StorageBackend{}
var configJSON []byte
err := r.db.conn.QueryRowContext(ctx, query, workspaceID).Scan(
&storage.ID,
&storage.WorkspaceID,
&storage.OwnerID,
&storage.Name,
&storage.Type,
&configJSON,
&storage.Description,
&storage.IsDefault,
&storage.IsShared,
&storage.CreatedAt,
&storage.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("failed to get default storage: %w", err)
}
if err := json.Unmarshal(configJSON, &storage.Config); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
return storage, nil
}
// Update 更新存储后端
func (r *StorageRepository) Update(ctx context.Context, storage *entity.StorageBackend) error {
storage.UpdatedAt = time.Now()
configJSON, err := json.Marshal(storage.Config)
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
query := `
UPDATE storage_backends
SET name = $1, type = $2, config = $3, description = $4, is_default = $5, is_shared = $6, updated_at = $7
WHERE id = $8
`
result, err := r.db.conn.ExecContext(ctx, query,
storage.Name,
storage.Type,
configJSON,
storage.Description,
storage.IsDefault,
storage.IsShared,
storage.UpdatedAt,
storage.ID,
)
if err != nil {
return fmt.Errorf("failed to update storage: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rows == 0 {
return entity.ErrStorageNotFound
}
return nil
}
// Delete 删除存储后端
func (r *StorageRepository) Delete(ctx context.Context, id string) error {
query := `DELETE FROM storage_backends WHERE id = $1`
result, err := r.db.conn.ExecContext(ctx, query, id)
if err != nil {
return fmt.Errorf("failed to delete storage: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rows == 0 {
return entity.ErrStorageNotFound
}
return nil
}
// List 列出所有存储后端(管理员用)
func (r *StorageRepository) List(ctx context.Context) ([]*entity.StorageBackend, error) {
query := `
SELECT id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at
FROM storage_backends
ORDER BY workspace_id, name
`
rows, err := r.db.conn.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to list storage: %w", err)
}
defer rows.Close()
return r.scanStorages(rows)
}
// scanStorages 扫描多行结果
func (r *StorageRepository) scanStorages(rows *sql.Rows) ([]*entity.StorageBackend, error) {
storages := make([]*entity.StorageBackend, 0)
for rows.Next() {
storage := &entity.StorageBackend{}
var configJSON []byte
err := rows.Scan(
&storage.ID,
&storage.WorkspaceID,
&storage.OwnerID,
&storage.Name,
&storage.Type,
&configJSON,
&storage.Description,
&storage.IsDefault,
&storage.IsShared,
&storage.CreatedAt,
&storage.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan storage: %w", err)
}
if err := json.Unmarshal(configJSON, &storage.Config); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
storages = append(storages, storage)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
return storages, nil
}

View File

@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"fmt"
"log"
"time"
"github.com/google/uuid"
@ -27,9 +28,14 @@ func (r *UserRepository) Create(ctx context.Context, user *entity.User) error {
user.ID = uuid.New().String()
}
// 设置默认值
if user.IsActive {
user.IsActive = true
}
query := `
INSERT INTO users (id, username, password_hash, email, revoked_after, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)
INSERT INTO users (id, username, password_hash, email, role, workspace_id, is_active, must_change_password, revoked_after, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
`
_, err := r.db.conn.ExecContext(ctx, query,
@ -37,6 +43,10 @@ func (r *UserRepository) Create(ctx context.Context, user *entity.User) error {
user.Username,
user.PasswordHash,
user.Email,
user.Role,
user.WorkspaceID,
user.IsActive,
user.MustChangePassword,
user.RevokedAfter,
user.CreatedAt,
user.UpdatedAt,
@ -52,22 +62,34 @@ func (r *UserRepository) Create(ctx context.Context, user *entity.User) error {
// GetByID 根据 ID 获取用户
func (r *UserRepository) GetByID(ctx context.Context, id string) (*entity.User, error) {
query := `
SELECT id, username, password_hash, email, revoked_after, created_at, updated_at
SELECT id, username, password_hash, email, role, workspace_id, is_active, must_change_password, revoked_after, created_at, updated_at
FROM users
WHERE id = $1
`
user := &entity.User{}
var workspaceID sql.NullString
err := r.db.conn.QueryRowContext(ctx, query, id).Scan(
&user.ID,
&user.Username,
&user.PasswordHash,
&user.Email,
&user.Role,
&workspaceID,
&user.IsActive,
&user.MustChangePassword,
&user.RevokedAfter,
&user.CreatedAt,
&user.UpdatedAt,
)
// Handle NULL workspace_id
if workspaceID.Valid {
user.WorkspaceID = workspaceID.String
} else {
user.WorkspaceID = ""
}
if err == sql.ErrNoRows {
return nil, entity.ErrUserNotFound
}
@ -80,30 +102,50 @@ func (r *UserRepository) GetByID(ctx context.Context, id string) (*entity.User,
// GetByUsername 根据用户名获取用户
func (r *UserRepository) GetByUsername(ctx context.Context, username string) (*entity.User, error) {
log.Printf("[DEBUG] GetByUsername called with username: %q", username)
query := `
SELECT id, username, password_hash, email, revoked_after, created_at, updated_at
SELECT id, username, password_hash, email, role, workspace_id, is_active, must_change_password, revoked_after, created_at, updated_at
FROM users
WHERE username = $1
`
log.Printf("[DEBUG] Executing query: %s with param: %s", query, username)
user := &entity.User{}
var workspaceID sql.NullString
err := r.db.conn.QueryRowContext(ctx, query, username).Scan(
&user.ID,
&user.Username,
&user.PasswordHash,
&user.Email,
&user.Role,
&workspaceID,
&user.IsActive,
&user.MustChangePassword,
&user.RevokedAfter,
&user.CreatedAt,
&user.UpdatedAt,
)
// Handle NULL workspace_id
if workspaceID.Valid {
user.WorkspaceID = workspaceID.String
} else {
user.WorkspaceID = ""
}
log.Printf("[DEBUG] Query result - err: %v", err)
if err == sql.ErrNoRows {
log.Printf("[DEBUG] User not found in DB")
return nil, entity.ErrUserNotFound
}
if err != nil {
log.Printf("[DEBUG] Scan error: %v", err)
return nil, fmt.Errorf("failed to get user: %w", err)
}
log.Printf("[DEBUG] Found user: %+v", user)
return user, nil
}
@ -113,14 +155,18 @@ func (r *UserRepository) Update(ctx context.Context, user *entity.User) error {
query := `
UPDATE users
SET username = $1, password_hash = $2, email = $3, revoked_after = $4, updated_at = $5
WHERE id = $6
SET username = $1, password_hash = $2, email = $3, role = $4, workspace_id = $5, is_active = $6, must_change_password = $7, revoked_after = $8, updated_at = $9
WHERE id = $10
`
result, err := r.db.conn.ExecContext(ctx, query,
user.Username,
user.PasswordHash,
user.Email,
user.Role,
user.WorkspaceID,
user.IsActive,
user.MustChangePassword,
user.RevokedAfter,
user.UpdatedAt,
user.ID,
@ -166,7 +212,7 @@ func (r *UserRepository) Delete(ctx context.Context, id string) error {
// List 列出所有用户
func (r *UserRepository) List(ctx context.Context) ([]*entity.User, error) {
query := `
SELECT id, username, password_hash, email, revoked_after, created_at, updated_at
SELECT id, username, password_hash, email, role, workspace_id, is_active, must_change_password, revoked_after, created_at, updated_at
FROM users
ORDER BY created_at DESC
`
@ -185,6 +231,98 @@ func (r *UserRepository) List(ctx context.Context) ([]*entity.User, error) {
&user.Username,
&user.PasswordHash,
&user.Email,
&user.Role,
&user.WorkspaceID,
&user.IsActive,
&user.MustChangePassword,
&user.RevokedAfter,
&user.CreatedAt,
&user.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan user: %w", err)
}
users = append(users, user)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
return users, nil
}
// ListByWorkspace 列出指定 workspace 的用户
func (r *UserRepository) ListByWorkspace(ctx context.Context, workspaceID string) ([]*entity.User, error) {
query := `
SELECT id, username, password_hash, email, role, workspace_id, is_active, must_change_password, revoked_after, created_at, updated_at
FROM users
WHERE workspace_id = $1
ORDER BY created_at DESC
`
rows, err := r.db.conn.QueryContext(ctx, query, workspaceID)
if err != nil {
return nil, fmt.Errorf("failed to list users by workspace: %w", err)
}
defer rows.Close()
users := make([]*entity.User, 0)
for rows.Next() {
user := &entity.User{}
err := rows.Scan(
&user.ID,
&user.Username,
&user.PasswordHash,
&user.Email,
&user.Role,
&user.WorkspaceID,
&user.IsActive,
&user.MustChangePassword,
&user.RevokedAfter,
&user.CreatedAt,
&user.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan user: %w", err)
}
users = append(users, user)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
return users, nil
}
// ListActive 仅列出活跃用户
func (r *UserRepository) ListActive(ctx context.Context) ([]*entity.User, error) {
query := `
SELECT id, username, password_hash, email, role, workspace_id, is_active, must_change_password, revoked_after, created_at, updated_at
FROM users
WHERE is_active = TRUE
ORDER BY created_at DESC
`
rows, err := r.db.conn.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to list active users: %w", err)
}
defer rows.Close()
users := make([]*entity.User, 0)
for rows.Next() {
user := &entity.User{}
err := rows.Scan(
&user.ID,
&user.Username,
&user.PasswordHash,
&user.Email,
&user.Role,
&user.WorkspaceID,
&user.IsActive,
&user.MustChangePassword,
&user.RevokedAfter,
&user.CreatedAt,
&user.UpdatedAt,

View File

@ -0,0 +1,287 @@
package postgres
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/google/uuid"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
// ValuesTemplateRepository PostgreSQL Values 模板仓储实现
type ValuesTemplateRepository struct {
db *DB
}
// NewValuesTemplateRepository 创建 PostgreSQL Values 模板仓储
func NewValuesTemplateRepository(db *DB) repository.ValuesTemplateRepository {
return &ValuesTemplateRepository{db: db}
}
// Create 创建 Values 模板
func (r *ValuesTemplateRepository) Create(ctx context.Context, template *entity.ValuesTemplate) error {
if template.ID == "" {
template.ID = uuid.New().String()
}
query := `
INSERT INTO values_templates (id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
`
_, err := r.db.conn.ExecContext(ctx, query,
template.ID,
template.WorkspaceID,
template.OwnerID,
template.ChartReferenceID,
template.Name,
template.Description,
template.ValuesYAML,
template.Version,
template.IsDefault,
template.CreatedAt,
template.UpdatedAt,
)
if err != nil {
return fmt.Errorf("failed to create values template: %w", err)
}
return nil
}
// GetByID 根据 ID 获取 Values 模板
func (r *ValuesTemplateRepository) GetByID(ctx context.Context, id string) (*entity.ValuesTemplate, error) {
query := `
SELECT id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at
FROM values_templates
WHERE id = $1
`
template := &entity.ValuesTemplate{}
err := r.db.conn.QueryRowContext(ctx, query, id).Scan(
&template.ID,
&template.WorkspaceID,
&template.OwnerID,
&template.ChartReferenceID,
&template.Name,
&template.Description,
&template.ValuesYAML,
&template.Version,
&template.IsDefault,
&template.CreatedAt,
&template.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, entity.ErrTemplateNotFound
}
if err != nil {
return nil, fmt.Errorf("failed to get values template: %w", err)
}
return template, nil
}
// GetByWorkspace 获取 workspace 的所有 Values 模板
func (r *ValuesTemplateRepository) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.ValuesTemplate, error) {
query := `
SELECT id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at
FROM values_templates
WHERE workspace_id = $1
ORDER BY chart_reference_id, name, version DESC
`
rows, err := r.db.conn.QueryContext(ctx, query, workspaceID)
if err != nil {
return nil, fmt.Errorf("failed to list values templates: %w", err)
}
defer rows.Close()
return r.scanValuesTemplates(rows)
}
// GetByChartReference 获取 Chart Reference 的所有 Values 模板
func (r *ValuesTemplateRepository) GetByChartReference(ctx context.Context, chartRefID string) ([]*entity.ValuesTemplate, error) {
query := `
SELECT id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at
FROM values_templates
WHERE chart_reference_id = $1
ORDER BY name, version DESC
`
rows, err := r.db.conn.QueryContext(ctx, query, chartRefID)
if err != nil {
return nil, fmt.Errorf("failed to list values templates: %w", err)
}
defer rows.Close()
return r.scanValuesTemplates(rows)
}
// GetByName 根据名称获取 Values 模板(获取最新版本)
func (r *ValuesTemplateRepository) GetByName(ctx context.Context, workspaceID, chartRefID, name string) (*entity.ValuesTemplate, error) {
query := `
SELECT id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at
FROM values_templates
WHERE workspace_id = $1 AND chart_reference_id = $2 AND name = $3
ORDER BY version DESC
LIMIT 1
`
template := &entity.ValuesTemplate{}
err := r.db.conn.QueryRowContext(ctx, query, workspaceID, chartRefID, name).Scan(
&template.ID,
&template.WorkspaceID,
&template.OwnerID,
&template.ChartReferenceID,
&template.Name,
&template.Description,
&template.ValuesYAML,
&template.Version,
&template.IsDefault,
&template.CreatedAt,
&template.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, entity.ErrTemplateNotFound
}
if err != nil {
return nil, fmt.Errorf("failed to get values template: %w", err)
}
return template, nil
}
// GetHistory 获取模板的版本历史
func (r *ValuesTemplateRepository) GetHistory(ctx context.Context, chartRefID, name string) ([]*entity.ValuesTemplate, error) {
query := `
SELECT id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at
FROM values_templates
WHERE chart_reference_id = $1 AND name = $2
ORDER BY version DESC
`
rows, err := r.db.conn.QueryContext(ctx, query, chartRefID, name)
if err != nil {
return nil, fmt.Errorf("failed to get values template history: %w", err)
}
defer rows.Close()
return r.scanValuesTemplates(rows)
}
// Update 更新 Values 模板(自动递增版本)
func (r *ValuesTemplateRepository) Update(ctx context.Context, template *entity.ValuesTemplate) error {
// 获取当前最大版本号
var maxVersion int
err := r.db.conn.QueryRowContext(ctx,
"SELECT COALESCE(MAX(version), 0) FROM values_templates WHERE chart_reference_id = $1 AND name = $2",
template.ChartReferenceID, template.Name,
).Scan(&maxVersion)
if err != nil {
return fmt.Errorf("failed to get max version: %w", err)
}
// 生成新 ID 用于新版本
newID := uuid.New().String()
template.Version = maxVersion + 1
template.UpdatedAt = time.Now()
template.CreatedAt = time.Now() // 新版本的创建时间
query := `
INSERT INTO values_templates (id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
`
_, err = r.db.conn.ExecContext(ctx, query,
newID,
template.WorkspaceID,
template.OwnerID,
template.ChartReferenceID,
template.Name,
template.Description,
template.ValuesYAML,
template.Version,
template.IsDefault,
template.CreatedAt,
template.UpdatedAt,
)
if err != nil {
return fmt.Errorf("failed to update values template: %w", err)
}
return nil
}
// Delete 删除 Values 模板
func (r *ValuesTemplateRepository) Delete(ctx context.Context, id string) error {
// 获取模板信息
template, err := r.GetByID(ctx, id)
if err != nil {
return err
}
// 删除该名称的所有版本
query := `DELETE FROM values_templates WHERE chart_reference_id = $1 AND name = $2`
_, err = r.db.conn.ExecContext(ctx, query, template.ChartReferenceID, template.Name)
if err != nil {
return fmt.Errorf("failed to delete values template: %w", err)
}
return nil
}
// List 列出所有 Values 模板(管理员用)
func (r *ValuesTemplateRepository) List(ctx context.Context) ([]*entity.ValuesTemplate, error) {
query := `
SELECT id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at
FROM values_templates
ORDER BY workspace_id, chart_reference_id, name, version DESC
`
rows, err := r.db.conn.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to list values templates: %w", err)
}
defer rows.Close()
return r.scanValuesTemplates(rows)
}
// scanValuesTemplates 扫描多行结果
func (r *ValuesTemplateRepository) scanValuesTemplates(rows *sql.Rows) ([]*entity.ValuesTemplate, error) {
templates := make([]*entity.ValuesTemplate, 0)
for rows.Next() {
template := &entity.ValuesTemplate{}
err := rows.Scan(
&template.ID,
&template.WorkspaceID,
&template.OwnerID,
&template.ChartReferenceID,
&template.Name,
&template.Description,
&template.ValuesYAML,
&template.Version,
&template.IsDefault,
&template.CreatedAt,
&template.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan values template: %w", err)
}
templates = append(templates, template)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
return templates, nil
}

View File

@ -0,0 +1,197 @@
package postgres
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/google/uuid"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
// WorkspaceRepository PostgreSQL Workspace 仓储实现
type WorkspaceRepository struct {
db *DB
}
// NewWorkspaceRepository 创建 PostgreSQL Workspace 仓储
func NewWorkspaceRepository(db *DB) repository.WorkspaceRepository {
return &WorkspaceRepository{db: db}
}
// Create 创建 Workspace
func (r *WorkspaceRepository) Create(ctx context.Context, workspace *entity.Workspace) error {
if workspace.ID == "" {
workspace.ID = uuid.New().String()
}
query := `
INSERT INTO workspaces (id, name, description, created_by, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6)
`
_, err := r.db.conn.ExecContext(ctx, query,
workspace.ID,
workspace.Name,
workspace.Description,
workspace.CreatedBy,
workspace.CreatedAt,
workspace.UpdatedAt,
)
if err != nil {
return fmt.Errorf("failed to create workspace: %w", err)
}
return nil
}
// GetByID 根据 ID 获取 Workspace
func (r *WorkspaceRepository) GetByID(ctx context.Context, id string) (*entity.Workspace, error) {
query := `
SELECT id, name, description, created_by, created_at, updated_at
FROM workspaces
WHERE id = $1
`
workspace := &entity.Workspace{}
err := r.db.conn.QueryRowContext(ctx, query, id).Scan(
&workspace.ID,
&workspace.Name,
&workspace.Description,
&workspace.CreatedBy,
&workspace.CreatedAt,
&workspace.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, entity.ErrWorkspaceNotFound
}
if err != nil {
return nil, fmt.Errorf("failed to get workspace: %w", err)
}
return workspace, nil
}
// GetByName 根据名称获取 Workspace
func (r *WorkspaceRepository) GetByName(ctx context.Context, name string) (*entity.Workspace, error) {
query := `
SELECT id, name, description, created_by, created_at, updated_at
FROM workspaces
WHERE name = $1
`
workspace := &entity.Workspace{}
err := r.db.conn.QueryRowContext(ctx, query, name).Scan(
&workspace.ID,
&workspace.Name,
&workspace.Description,
&workspace.CreatedBy,
&workspace.CreatedAt,
&workspace.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, entity.ErrWorkspaceNotFound
}
if err != nil {
return nil, fmt.Errorf("failed to get workspace: %w", err)
}
return workspace, nil
}
// Update 更新 Workspace
func (r *WorkspaceRepository) Update(ctx context.Context, workspace *entity.Workspace) error {
workspace.UpdatedAt = time.Now()
query := `
UPDATE workspaces
SET name = $1, description = $2, updated_at = $3
WHERE id = $4
`
result, err := r.db.conn.ExecContext(ctx, query,
workspace.Name,
workspace.Description,
workspace.UpdatedAt,
workspace.ID,
)
if err != nil {
return fmt.Errorf("failed to update workspace: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rows == 0 {
return entity.ErrWorkspaceNotFound
}
return nil
}
// Delete 删除 Workspace
func (r *WorkspaceRepository) Delete(ctx context.Context, id string) error {
query := `DELETE FROM workspaces WHERE id = $1`
result, err := r.db.conn.ExecContext(ctx, query, id)
if err != nil {
return fmt.Errorf("failed to delete workspace: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rows == 0 {
return entity.ErrWorkspaceNotFound
}
return nil
}
// List 列出所有 Workspace
func (r *WorkspaceRepository) List(ctx context.Context) ([]*entity.Workspace, error) {
query := `
SELECT id, name, description, created_by, created_at, updated_at
FROM workspaces
ORDER BY created_at DESC
`
rows, err := r.db.conn.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to list workspaces: %w", err)
}
defer rows.Close()
workspaces := make([]*entity.Workspace, 0)
for rows.Next() {
workspace := &entity.Workspace{}
err := rows.Scan(
&workspace.ID,
&workspace.Name,
&workspace.Description,
&workspace.CreatedBy,
&workspace.CreatedAt,
&workspace.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan workspace: %w", err)
}
workspaces = append(workspaces, workspace)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
return workspaces, nil
}

View File

@ -0,0 +1,90 @@
package entity
import (
"encoding/json"
"time"
)
// AuditAction 审计操作类型
type AuditAction string
const (
AuditActionCreate AuditAction = "create"
AuditActionUpdate AuditAction = "update"
AuditActionDelete AuditAction = "delete"
AuditActionDeploy AuditAction = "deploy"
AuditActionScale AuditAction = "scale"
AuditActionLogin AuditAction = "login"
AuditActionLogout AuditAction = "logout"
AuditActionChangePassword AuditAction = "change_password"
)
// AuditResourceType 审计资源类型
type AuditResourceType string
const (
AuditResourceUser AuditResourceType = "user"
AuditResourceWorkspace AuditResourceType = "workspace"
AuditResourceQuota AuditResourceType = "quota"
AuditResourceCluster AuditResourceType = "cluster"
AuditResourceRegistry AuditResourceType = "registry"
AuditResourceInstance AuditResourceType = "instance"
AuditResourceStorage AuditResourceType = "storage"
AuditResourceTemplate AuditResourceType = "template"
)
// AuditLog 审计日志实体
type AuditLog struct {
ID string
WorkspaceID string
UserID string
Action AuditAction
ResourceType AuditResourceType
ResourceID string
ResourceName string
Details map[string]interface{}
IPAddress string
UserAgent string
CreatedAt time.Time
}
// NewAuditLog 创建新审计日志
func NewAuditLog(workspaceID, userID string, action AuditAction, resourceType AuditResourceType) *AuditLog {
now := time.Now()
return &AuditLog{
WorkspaceID: workspaceID,
UserID: userID,
Action: action,
ResourceType: resourceType,
CreatedAt: now,
}
}
// SetResource 设置关联资源
func (a *AuditLog) SetResource(resourceID, resourceName string) {
a.ResourceID = resourceID
a.ResourceName = resourceName
}
// SetDetails 设置详细信息
func (a *AuditLog) SetDetails(details map[string]interface{}) {
a.Details = details
}
// SetClientInfo 设置客户端信息
func (a *AuditLog) SetClientInfo(ipAddress, userAgent string) {
a.IPAddress = ipAddress
a.UserAgent = userAgent
}
// DetailsJSON 将详情转为 JSON 字符串
func (a *AuditLog) DetailsJSON() (string, error) {
if a.Details == nil {
return "{}", nil
}
data, err := json.Marshal(a.Details)
if err != nil {
return "", err
}
return string(data), nil
}

View File

@ -0,0 +1,41 @@
package entity
import (
"time"
)
// ChartReference Chart 引用实体
type ChartReference struct {
ID string
WorkspaceID string
RegistryID string
Repository string
ChartName string
Description string
IsEnabled bool
CreatedAt time.Time
UpdatedAt time.Time
}
// NewChartReference 创建新 Chart 引用
func NewChartReference(workspaceID, registryID, repository, chartName, description string) *ChartReference {
now := time.Now()
return &ChartReference{
WorkspaceID: workspaceID,
RegistryID: registryID,
Repository: repository,
ChartName: chartName,
Description: description,
IsEnabled: true,
CreatedAt: now,
UpdatedAt: now,
}
}
// Validate 验证 Chart 引用数据
func (c *ChartReference) Validate() error {
if c.Repository == "" {
return ErrInvalidChart
}
return nil
}

View File

@ -4,28 +4,49 @@ import (
"time"
)
// IsolationMode 集群隔离模式
type IsolationMode string
const (
IsolationModeNamespace IsolationMode = "namespace" // 共享集群模式,多 workspace 使用不同 namespace
IsolationModeCluster IsolationMode = "cluster" // 私有集群模式,每个 workspace 独立集群
)
// Cluster Kubernetes 集群领域实体
type Cluster struct {
ID string
Name string
Host string // Kubernetes API Server URL
CAData string // Base64 encoded CA certificate
CertData string // Base64 encoded client certificate
KeyData string // Base64 encoded client key
Token string // Bearer token (alternative to cert auth)
Description string
CreatedAt time.Time
UpdatedAt time.Time
ID string
WorkspaceID string // 所属 workspaceNULL 表示全局共享
OwnerID string // 创建者用户 ID
Name string
Host string // Kubernetes API Server URL
CAData string // Base64 encoded CA certificate
CertData string // Base64 encoded client certificate
KeyData string // Base64 encoded client key
Token string // Bearer token (alternative to cert auth)
Description string
// 隔离模式
IsolationMode IsolationMode // 'namespace' | 'cluster'
DefaultNamespace string // 当 isolation_mode=namespace 时的默认 namespace 前缀
IsShared bool // 是否为共享集群admin 创建供多 workspace 使用)
CreatedAt time.Time
UpdatedAt time.Time
}
// NewCluster 创建新集群
func NewCluster(name, host string) *Cluster {
func NewCluster(workspaceID, ownerID, name, host string) *Cluster {
now := time.Now()
return &Cluster{
Name: name,
Host: host,
CreatedAt: now,
UpdatedAt: now,
WorkspaceID: workspaceID,
OwnerID: ownerID,
Name: name,
Host: host,
IsolationMode: IsolationModeNamespace, // 默认 namespace 隔离模式
DefaultNamespace: workspaceID, // 默认使用 workspace ID 作为 namespace 前缀
IsShared: false,
CreatedAt: now,
UpdatedAt: now,
}
}
@ -63,11 +84,35 @@ func (c *Cluster) Validate() error {
if c.Host == "" {
return ErrInvalidClusterHost
}
// 必须有认证方式:证书或 Token
if (c.CertData == "" || c.KeyData == "") && c.Token == "" {
return ErrInvalidClusterAuth
// 检查是否有 kubeconfig 格式(完整的 kubeconfig 在 CAData 中)
hasKubeconfig := len(c.CAData) > 100 && (c.CAData[:11] == "apiVersion:" || c.CAData[:5] == "kind:")
// 认证方式证书、Token、kubeconfig 或空(使用本地 kubeconfig
hasCertAuth := c.CertData != "" && c.KeyData != ""
hasToken := c.Token != ""
hasNoAuth := c.CertData == "" && c.KeyData == "" && c.Token == ""
// 如果有 kubeconfig 格式,或有证书,或有 token或没有凭证依赖 TestConnection 使用本地 kubeconfig都是有效的
if hasKubeconfig || hasCertAuth || hasToken || hasNoAuth {
return nil
}
return nil
return ErrInvalidClusterAuth
}
// GetNamespace 获取部署用的 namespace
// namespace 隔离模式: {workspace_id}-{instance_name} 或 {default_namespace}-{username}
// cluster 隔离模式: 使用 workspace 的默认 namespace
func (c *Cluster) GetNamespace(workspaceName, instanceName string) string {
if c.IsolationMode == IsolationModeCluster {
return c.DefaultNamespace
}
// namespace 隔离模式
if c.DefaultNamespace != "" {
return c.DefaultNamespace + "-" + instanceName
}
return workspaceName + "-" + instanceName
}
// GetKubeConfig 生成 kubeconfig 内容

View File

@ -37,4 +37,32 @@ var (
ErrArtifactNotFound = errors.New("artifact not found")
ErrRepositoryNotFound = errors.New("repository not found")
ErrValuesSchemaNotFound = errors.New("values schema not found")
ErrValuesNotFound = errors.New("values not found")
// Workspace errors
ErrInvalidWorkspaceName = errors.New("invalid workspace name")
ErrWorkspaceNotFound = errors.New("workspace not found")
ErrWorkspaceExists = errors.New("workspace already exists")
// Quota errors
ErrQuotaExceeded = errors.New("quota exceeded")
ErrInvalidQuota = errors.New("invalid quota")
// Storage errors
ErrInvalidStorageName = errors.New("invalid storage name")
ErrStorageNotFound = errors.New("storage not found")
ErrStorageExists = errors.New("storage already exists")
// Chart Reference errors
ErrInvalidChartReferenceName = errors.New("invalid chart reference name")
ErrChartReferenceNotFound = errors.New("chart reference not found")
ErrChartReferenceExists = errors.New("chart reference already exists")
// Template errors
ErrInvalidTemplateName = errors.New("invalid template name")
ErrTemplateNotFound = errors.New("template not found")
ErrTemplateExists = errors.New("template already exists")
// Permission errors
ErrPermissionDenied = errors.New("permission denied")
)

View File

@ -1,7 +1,9 @@
package entity
import (
"strings"
"time"
"unicode"
)
// InstanceStatus 实例状态
@ -33,43 +35,65 @@ const (
// Instance Helm 应用实例领域实体
type Instance struct {
ID string
ClusterID string
Name string // Helm Release Name
Namespace string
RegistryID string
Repository string // OCI Repository (e.g., charts/app)
Chart string // Chart Name
Version string // Chart Version
Description string
Values map[string]interface{} // Helm Values (JSON)
ValuesYAML string // Helm Values (YAML format)
Status InstanceStatus
StatusReason string
LastOperation InstanceOperation
LastError string
Revision int // Helm Release Revision
CreatedAt time.Time
UpdatedAt time.Time
ID string
WorkspaceID string // 所属 workspace
OwnerID string // 创建者用户 ID
ClusterID string
RegistryID string
ChartReferenceID string // 引用的 Chart 引用
ValuesTemplateID string // 使用的 Values 模板
Name string // Helm Release Name
Namespace string
Repository string // OCI Repository (e.g., charts/app)
Chart string // Chart Name
Version string // Chart Version
Description string
Values map[string]interface{} // Helm Values (JSON)
ValuesYAML string // Helm Values (YAML format)
UserOverrideYAML string // 用户额外覆盖的配置
Status InstanceStatus
StatusReason string
LastOperation InstanceOperation
LastError string
Revision int // Helm Release Revision
// 资源使用统计Helm 安装时从集群获取并更新)
CPURequested float64 // CPU 请求量 (cores)
MemoryRequested string // 内存请求量 (e.g., "2Gi")
GPURequested float64 // GPU 请求量 (cards)
GPUMemoryRequested string // GPU 内存请求量 (e.g., "16Gi")
CreatedAt time.Time
UpdatedAt time.Time
}
// NewInstance 创建新实例
func NewInstance(clusterID, name, namespace, registryID, repository, chart, version string) *Instance {
func NewInstance(workspaceID, ownerID, clusterID, registryID, chartReferenceID, valuesTemplateID, name, namespace, repository, chart, version string) *Instance {
now := time.Now()
return &Instance{
ClusterID: clusterID,
Name: name,
Namespace: namespace,
RegistryID: registryID,
Repository: repository,
Chart: chart,
Version: version,
Status: StatusPending,
StatusReason: "Pending install",
LastOperation: OperationInstall,
Revision: 1,
CreatedAt: now,
UpdatedAt: now,
WorkspaceID: workspaceID,
OwnerID: ownerID,
ClusterID: clusterID,
RegistryID: registryID,
ChartReferenceID: chartReferenceID,
ValuesTemplateID: valuesTemplateID,
Name: name,
Namespace: namespace,
Repository: repository,
Chart: chart,
Version: version,
Status: StatusPending,
StatusReason: "Pending install",
LastOperation: OperationInstall,
Revision: 1,
CPURequested: 0,
MemoryRequested: "0Mi",
GPURequested: 0,
GPUMemoryRequested: "0Mi",
CreatedAt: now,
UpdatedAt: now,
}
}
@ -154,13 +178,43 @@ func (i *Instance) Upgrade(version string, values map[string]interface{}) {
i.BeginOperation(OperationUpgrade, "Pending upgrade")
}
// ValidateReleaseName 验证 Helm Release 名称是否符合 RFC 1123 DNS 子域名规范
// Helm release 名称必须:
// - 只能包含小写字母a-z、数字0-9和连字符-
// - 不能以连字符开头或结尾
// - 长度不超过 53 个字符
func ValidateReleaseName(name string) error {
if name == "" {
return ErrInvalidInstanceName
}
// 检查长度RFC 1123 DNS 子域名最大长度为 63但 Helm 限制为 53
if len(name) > 53 {
return ErrInvalidInstanceName
}
// 不能以连字符开头或结尾
if strings.HasPrefix(name, "-") || strings.HasSuffix(name, "-") {
return ErrInvalidInstanceName
}
// 只能包含小写字母、数字和连字符
for _, r := range name {
if !(unicode.IsLower(r) || unicode.IsDigit(r) || r == '-') {
return ErrInvalidInstanceName
}
}
return nil
}
// Validate 验证实例配置
func (i *Instance) Validate() error {
if i.ClusterID == "" {
return ErrInvalidClusterID
}
if i.Name == "" {
return ErrInvalidInstanceName
if err := ValidateReleaseName(i.Name); err != nil {
return err
}
if i.Namespace == "" {
return ErrInvalidNamespace

View File

@ -0,0 +1,79 @@
package entity
import (
"time"
)
// ResourceType 资源类型
type ResourceType string
const (
ResourceCPU ResourceType = "cpu"
ResourceGPU ResourceType = "gpu"
ResourceGPUMemory ResourceType = "gpu_memory"
)
// WorkspaceQuota 工作空间配额实体
type WorkspaceQuota struct {
ID string
WorkspaceID string
ResourceType ResourceType
HardLimit float64 // 硬限制0表示无限制
SoftLimit float64 // 软限制(警告阈值)
Used float64 // 当前使用量
CreatedAt time.Time
UpdatedAt time.Time
}
// NewWorkspaceQuota 创建新配额
func NewWorkspaceQuota(workspaceID string, resourceType ResourceType, hardLimit, softLimit float64) *WorkspaceQuota {
now := time.Now()
return &WorkspaceQuota{
WorkspaceID: workspaceID,
ResourceType: resourceType,
HardLimit: hardLimit,
SoftLimit: softLimit,
Used: 0,
CreatedAt: now,
UpdatedAt: now,
}
}
// CanAllocate 检查是否可以分配指定资源量
func (q *WorkspaceQuota) CanAllocate(amount float64) bool {
if q.HardLimit == 0 {
return true // 无限制
}
return q.Used+amount <= q.HardLimit
}
// Allocate 分配资源
func (q *WorkspaceQuota) Allocate(amount float64) {
q.Used += amount
q.UpdatedAt = time.Now()
}
// Release 释放资源
func (q *WorkspaceQuota) Release(amount float64) {
q.Used -= amount
if q.Used < 0 {
q.Used = 0
}
q.UpdatedAt = time.Now()
}
// IsOverLimit 检查是否超过硬限制
func (q *WorkspaceQuota) IsOverLimit() bool {
if q.HardLimit == 0 {
return false
}
return q.Used > q.HardLimit
}
// IsOverSoftLimit 检查是否超过软限制
func (q *WorkspaceQuota) IsOverSoftLimit() bool {
if q.SoftLimit == 0 {
return false
}
return q.Used > q.SoftLimit
}

View File

@ -7,24 +7,30 @@ import (
// Registry OCI Registry 领域实体
type Registry struct {
ID string
WorkspaceID string // 所属 workspaceNULL 表示全局共享
OwnerID string // 创建者用户 ID
Name string
URL string
Description string
Username string
Password string
Insecure bool // 是否跳过 TLS 验证
Insecure bool // 是否跳过 TLS 验证
IsShared bool // 是否为共享 Registryadmin 创建供多 workspace 使用)
CreatedAt time.Time
UpdatedAt time.Time
}
// NewRegistry 创建新 Registry
func NewRegistry(name, url string) *Registry {
func NewRegistry(workspaceID, ownerID, name, url string) *Registry {
now := time.Now()
return &Registry{
Name: name,
URL: url,
CreatedAt: now,
UpdatedAt: now,
WorkspaceID: workspaceID,
OwnerID: ownerID,
Name: name,
URL: url,
IsShared: false,
CreatedAt: now,
UpdatedAt: now,
}
}

View File

@ -0,0 +1,98 @@
package entity
import (
"encoding/json"
"time"
)
// StorageType 存储类型
type StorageType string
const (
StorageTypeNFS StorageType = "nfs"
StorageTypePV StorageType = "pv"
StorageTypeHostPath StorageType = "hostPath"
)
// StorageConfig 存储配置
type StorageConfig struct {
NFS *NFSConfig `json:"nfs,omitempty"`
PV *PVConfig `json:"pv,omitempty"`
HostPath *HostPathConfig `json:"hostPath,omitempty"`
}
// NFSConfig NFS 配置
type NFSConfig struct {
Server string `json:"server"`
Path string `json:"path"`
}
// PVConfig PV 配置
type PVConfig struct {
StorageClassName string `json:"storageClassName"`
Capacity string `json:"capacity"`
AccessModes []string `json:"accessModes"`
}
// HostPathConfig HostPath 配置
type HostPathConfig struct {
Path string `json:"path"`
}
// StorageBackend 存储后端实体
type StorageBackend struct {
ID string
WorkspaceID string
OwnerID string
Name string
Type StorageType
Config StorageConfig
Description string
IsDefault bool
IsShared bool
CreatedAt time.Time
UpdatedAt time.Time
}
// NewStorageBackend 创建新存储后端
func NewStorageBackend(workspaceID, ownerID, name string, storageType StorageType, config StorageConfig) *StorageBackend {
now := time.Now()
return &StorageBackend{
WorkspaceID: workspaceID,
OwnerID: ownerID,
Name: name,
Type: storageType,
Config: config,
IsDefault: false,
IsShared: false,
CreatedAt: now,
UpdatedAt: now,
}
}
// Validate 验证存储后端数据
func (s *StorageBackend) Validate() error {
if s.Name == "" {
return ErrInvalidStorageName
}
if s.Type == "" {
return ErrInvalidStorageName
}
return nil
}
// ConfigJSON 将配置转为 JSON 字符串
func (s *StorageBackend) ConfigJSON() (string, error) {
data, err := json.Marshal(s.Config)
if err != nil {
return "", err
}
return string(data), nil
}
// ParseConfigJSON 从 JSON 解析配置
func ParseConfigJSON(jsonStr string) (*StorageConfig, error) {
var config StorageConfig
err := json.Unmarshal([]byte(jsonStr), &config)
return &config, err
}

View File

@ -4,30 +4,58 @@ import (
"time"
)
// UserRole 用户角色
type UserRole string
const (
RoleAdmin UserRole = "admin"
RoleUser UserRole = "user"
)
// User 用户领域实体
type User struct {
ID string
Username string
PasswordHash string
Email string
RevokedAfter time.Time // 全局 Token 撤销时间
CreatedAt time.Time
UpdatedAt time.Time
ID string
Username string
PasswordHash string
Email string
Role UserRole // 用户角色: admin, user
WorkspaceID string // 所属工作空间admin 为空表示全局
IsActive bool // 账户是否激活
MustChangePassword bool // 首次登录必须修改密码
RevokedAfter time.Time // 全局 Token 撤销时间
CreatedAt time.Time
UpdatedAt time.Time
}
// NewUser 创建新用户
func NewUser(username, passwordHash, email string) *User {
now := time.Now()
return &User{
Username: username,
PasswordHash: passwordHash,
Email: email,
RevokedAfter: time.Unix(0, 0), // 初始值1970-01-01
CreatedAt: now,
UpdatedAt: now,
Username: username,
PasswordHash: passwordHash,
Email: email,
Role: RoleUser, // 默认普通用户
IsActive: true,
MustChangePassword: true, // 首次登录必须修改密码
RevokedAfter: time.Unix(0, 0), // 初始值1970-01-01
CreatedAt: now,
UpdatedAt: now,
}
}
// IsAdmin 判断是否为管理员
func (u *User) IsAdmin() bool {
return u.Role == RoleAdmin
}
// CanAccessWorkspace 检查是否可以访问指定工作空间
func (u *User) CanAccessWorkspace(workspaceID string) bool {
if u.IsAdmin() {
return true // Admin 可以访问所有工作空间
}
return u.WorkspaceID == workspaceID
}
// UpdatePassword 更新密码(会触发全局登出)
func (u *User) UpdatePassword(newPasswordHash string) {
u.PasswordHash = newPasswordHash

View File

@ -0,0 +1,83 @@
package entity
import (
"time"
)
// ValuesTemplate Values 模板实体(带版本管理)
type ValuesTemplate struct {
ID string
WorkspaceID string
OwnerID string
ChartReferenceID string
Name string
Description string
ValuesYAML string
Version int // 模板版本号
IsDefault bool
CreatedAt time.Time
UpdatedAt time.Time
}
// NewValuesTemplate 创建新 Values 模板
func NewValuesTemplate(workspaceID, ownerID, chartReferenceID, name, valuesYAML string) *ValuesTemplate {
now := time.Now()
return &ValuesTemplate{
WorkspaceID: workspaceID,
OwnerID: ownerID,
ChartReferenceID: chartReferenceID,
Name: name,
ValuesYAML: valuesYAML,
Version: 1,
IsDefault: false,
CreatedAt: now,
UpdatedAt: now,
}
}
// Validate 验证 Values 模板数据
func (v *ValuesTemplate) Validate() error {
if v.Name == "" {
return ErrInvalidTemplateName
}
if v.ValuesYAML == "" {
return ErrInvalidTemplateName
}
return nil
}
// IncrementVersion 递增版本号
func (v *ValuesTemplate) IncrementVersion() {
v.Version++
v.UpdatedAt = time.Now()
}
// UserConfigOverride 用户配置覆盖实体
type UserConfigOverride struct {
ID string
WorkspaceID string
UserID string
TargetType string // 'storage', 'template', 'global'
TargetID string
Config map[string]interface{}
Priority int
IsActive bool
CreatedAt time.Time
UpdatedAt time.Time
}
// NewUserConfigOverride 创建新用户配置覆盖
func NewUserConfigOverride(workspaceID, userID, targetType, targetID string, config map[string]interface{}) *UserConfigOverride {
now := time.Now()
return &UserConfigOverride{
WorkspaceID: workspaceID,
UserID: userID,
TargetType: targetType,
TargetID: targetID,
Config: config,
Priority: 0,
IsActive: true,
CreatedAt: now,
UpdatedAt: now,
}
}

View File

@ -0,0 +1,35 @@
package entity
import (
"time"
)
// Workspace 工作空间实体
type Workspace struct {
ID string
Name string
Description string
CreatedBy string // 创建者用户 ID
CreatedAt time.Time
UpdatedAt time.Time
}
// NewWorkspace 创建新工作空间
func NewWorkspace(name, description, createdBy string) *Workspace {
now := time.Now()
return &Workspace{
Name: name,
Description: description,
CreatedBy: createdBy,
CreatedAt: now,
UpdatedAt: now,
}
}
// Validate 验证工作空间数据
func (w *Workspace) Validate() error {
if w.Name == "" {
return ErrInvalidWorkspaceName
}
return nil
}

View File

@ -0,0 +1,27 @@
package repository
import (
"context"
"github.com/ocdp/cluster-service/internal/domain/entity"
)
// AuditLogRepository 审计日志仓储接口
type AuditLogRepository interface {
// Create 创建审计日志
Create(ctx context.Context, log *entity.AuditLog) error
// GetByWorkspace 获取 workspace 的审计日志
GetByWorkspace(ctx context.Context, workspaceID string, limit int) ([]*entity.AuditLog, error)
// GetByUser 获取用户的审计日志
GetByUser(ctx context.Context, userID string, limit int) ([]*entity.AuditLog, error)
// GetByResource 获取资源的审计日志
GetByResource(ctx context.Context, resourceType entity.AuditResourceType, resourceID string, limit int) ([]*entity.AuditLog, error)
// List 列出审计日志(分页)
List(ctx context.Context, limit, offset int) ([]*entity.AuditLog, error)
// DeleteByWorkspace 删除 workspace 的审计日志
DeleteByWorkspace(ctx context.Context, workspaceID string) error
}

View File

@ -0,0 +1,33 @@
package repository
import (
"context"
"github.com/ocdp/cluster-service/internal/domain/entity"
)
// ChartReferenceRepository Chart 引用仓储接口
type ChartReferenceRepository interface {
// Create 创建 Chart 引用
Create(ctx context.Context, chartRef *entity.ChartReference) error
// GetByID 根据 ID 获取 Chart 引用
GetByID(ctx context.Context, id string) (*entity.ChartReference, error)
// GetByWorkspace 获取 workspace 的所有 Chart 引用
GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.ChartReference, error)
// GetByRegistry 获取 registry 的所有 Chart 引用
GetByRegistry(ctx context.Context, registryID string) ([]*entity.ChartReference, error)
// GetByName 根据名称获取 Chart 引用
GetByName(ctx context.Context, workspaceID, chartName string) (*entity.ChartReference, error)
// Update 更新 Chart 引用
Update(ctx context.Context, chartRef *entity.ChartReference) error
// Delete 删除 Chart 引用
Delete(ctx context.Context, id string) error
// List 列出所有 Chart 引用(管理员用)
List(ctx context.Context) ([]*entity.ChartReference, error)
}

View File

@ -9,20 +9,26 @@ import (
type ClusterRepository interface {
// Create 创建集群
Create(ctx context.Context, cluster *entity.Cluster) error
// GetByID 根据 ID 获取集群
GetByID(ctx context.Context, id string) (*entity.Cluster, error)
// GetByName 根据名称获取集群
GetByName(ctx context.Context, name string) (*entity.Cluster, error)
// Update 更新集群
Update(ctx context.Context, cluster *entity.Cluster) error
// Delete 删除集群
Delete(ctx context.Context, id string) error
// List 列出所有集群
List(ctx context.Context) ([]*entity.Cluster, error)
// GetByWorkspace 获取 workspace 的所有集群(包括共享集群)
GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.Cluster, error)
// GetShared 获取所有共享集群
GetShared(ctx context.Context) ([]*entity.Cluster, error)
}

View File

@ -9,23 +9,26 @@ import (
type InstanceRepository interface {
// Create 创建实例
Create(ctx context.Context, instance *entity.Instance) error
// GetByID 根据 ID 获取实例
GetByID(ctx context.Context, id string) (*entity.Instance, error)
// GetByClusterAndName 根据集群 ID 和名称获取实例
GetByClusterAndName(ctx context.Context, clusterID, name string) (*entity.Instance, error)
// Update 更新实例
Update(ctx context.Context, instance *entity.Instance) error
// Delete 删除实例
Delete(ctx context.Context, id string) error
// ListByCluster 列出指定集群的所有实例
ListByCluster(ctx context.Context, clusterID string) ([]*entity.Instance, error)
// List 列出所有实例
List(ctx context.Context) ([]*entity.Instance, error)
// GetByWorkspace 列出指定工作空间的所有实例(用于配额检查)
GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.Instance, error)
}

View File

@ -19,7 +19,10 @@ type OCIClient interface {
// GetValuesSchema 获取 Helm Chart 的 values schema
GetValuesSchema(ctx context.Context, registry *entity.Registry, repository, reference string) (string, error)
// GetValues 获取 Helm Chart 的 values.yaml
GetValues(ctx context.Context, registry *entity.Registry, repository, reference string) (string, error)
// PullArtifact 下载 artifact 到本地
PullArtifact(ctx context.Context, registry *entity.Registry, repository, reference, destPath string) error

View File

@ -0,0 +1,30 @@
package repository
import (
"context"
"github.com/ocdp/cluster-service/internal/domain/entity"
)
// QuotaRepository 配额仓储接口
type QuotaRepository interface {
// Create 创建配额
Create(ctx context.Context, quota *entity.WorkspaceQuota) error
// GetByID 根据 ID 获取配额
GetByID(ctx context.Context, id string) (*entity.WorkspaceQuota, error)
// GetByWorkspaceAndType 根据 workspace 和资源类型获取配额
GetByWorkspaceAndType(ctx context.Context, workspaceID string, resourceType entity.ResourceType) (*entity.WorkspaceQuota, error)
// GetByWorkspace 获取 workspace 的所有配额
GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.WorkspaceQuota, error)
// Update 更新配额
Update(ctx context.Context, quota *entity.WorkspaceQuota) error
// Delete 删除配额
Delete(ctx context.Context, id string) error
// DeleteByWorkspace 删除 workspace 的所有配额
DeleteByWorkspace(ctx context.Context, workspaceID string) error
}

View File

@ -0,0 +1,36 @@
package repository
import (
"context"
"github.com/ocdp/cluster-service/internal/domain/entity"
)
// StorageRepository 存储后端仓储接口
type StorageRepository interface {
// Create 创建存储后端
Create(ctx context.Context, storage *entity.StorageBackend) error
// GetByID 根据 ID 获取存储后端
GetByID(ctx context.Context, id string) (*entity.StorageBackend, error)
// GetByName 根据名称获取存储后端
GetByName(ctx context.Context, workspaceID, name string) (*entity.StorageBackend, error)
// GetByWorkspace 获取 workspace 的所有存储后端
GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.StorageBackend, error)
// GetShared 获取所有共享存储后端
GetShared(ctx context.Context) ([]*entity.StorageBackend, error)
// GetDefault 获取 workspace 的默认存储后端
GetDefault(ctx context.Context, workspaceID string) (*entity.StorageBackend, error)
// Update 更新存储后端
Update(ctx context.Context, storage *entity.StorageBackend) error
// Delete 删除存储后端
Delete(ctx context.Context, id string) error
// List 列出所有存储后端(管理员用)
List(ctx context.Context) ([]*entity.StorageBackend, error)
}

View File

@ -9,20 +9,26 @@ import (
type UserRepository interface {
// Create 创建用户
Create(ctx context.Context, user *entity.User) error
// GetByID 根据 ID 获取用户
GetByID(ctx context.Context, id string) (*entity.User, error)
// GetByUsername 根据用户名获取用户
GetByUsername(ctx context.Context, username string) (*entity.User, error)
// Update 更新用户
Update(ctx context.Context, user *entity.User) error
// Delete 删除用户
Delete(ctx context.Context, id string) error
// List 列出所有用户
List(ctx context.Context) ([]*entity.User, error)
// ListByWorkspace 列出指定 workspace 的用户
ListByWorkspace(ctx context.Context, workspaceID string) ([]*entity.User, error)
// ListActive 仅列出活跃用户
ListActive(ctx context.Context) ([]*entity.User, error)
}

View File

@ -0,0 +1,36 @@
package repository
import (
"context"
"github.com/ocdp/cluster-service/internal/domain/entity"
)
// ValuesTemplateRepository Values 模板仓储接口
type ValuesTemplateRepository interface {
// Create 创建 Values 模板
Create(ctx context.Context, template *entity.ValuesTemplate) error
// GetByID 根据 ID 获取 Values 模板
GetByID(ctx context.Context, id string) (*entity.ValuesTemplate, error)
// GetByWorkspace 获取 workspace 的所有 Values 模板
GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.ValuesTemplate, error)
// GetByChartReference 获取 Chart Reference 的所有 Values 模板
GetByChartReference(ctx context.Context, chartRefID string) ([]*entity.ValuesTemplate, error)
// GetByName 根据名称获取 Values 模板
GetByName(ctx context.Context, workspaceID, chartRefID, name string) (*entity.ValuesTemplate, error)
// GetHistory 获取模板的版本历史
GetHistory(ctx context.Context, chartRefID, name string) ([]*entity.ValuesTemplate, error)
// Update 更新 Values 模板(自动递增版本)
Update(ctx context.Context, template *entity.ValuesTemplate) error
// Delete 删除 Values 模板
Delete(ctx context.Context, id string) error
// List 列出所有 Values 模板(管理员用)
List(ctx context.Context) ([]*entity.ValuesTemplate, error)
}

View File

@ -0,0 +1,27 @@
package repository
import (
"context"
"github.com/ocdp/cluster-service/internal/domain/entity"
)
// WorkspaceRepository Workspace 仓储接口
type WorkspaceRepository interface {
// Create 创建 Workspace
Create(ctx context.Context, workspace *entity.Workspace) error
// GetByID 根据 ID 获取 Workspace
GetByID(ctx context.Context, id string) (*entity.Workspace, error)
// GetByName 根据名称获取 Workspace
GetByName(ctx context.Context, name string) (*entity.Workspace, error)
// Update 更新 Workspace
Update(ctx context.Context, workspace *entity.Workspace) error
// Delete 删除 Workspace
Delete(ctx context.Context, id string) error
// List 列出所有 Workspace
List(ctx context.Context) ([]*entity.Workspace, error)
}

View File

@ -68,6 +68,16 @@ func (s *ArtifactService) GetValuesSchema(ctx context.Context, registryID, repos
return s.ociClient.GetValuesSchema(ctx, registry, repository, reference)
}
// GetValues 获取 Helm Chart 的 values.yaml
func (s *ArtifactService) GetValues(ctx context.Context, registryID, repository, reference string) (string, error) {
registry, err := s.registryRepo.GetByID(ctx, registryID)
if err != nil {
return "", entity.ErrRegistryNotFound
}
return s.ociClient.GetValues(ctx, registry, repository, reference)
}
// PullArtifact 下载 artifact
func (s *ArtifactService) PullArtifact(ctx context.Context, registryID, repository, reference, destPath string) error {
registry, err := s.registryRepo.GetByID(ctx, registryID)

View File

@ -0,0 +1,71 @@
package service
import (
"context"
"time"
"github.com/google/uuid"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
// AuditService 审计日志领域服务
type AuditService struct {
auditLogRepo repository.AuditLogRepository
}
// NewAuditService 创建审计服务
func NewAuditService(auditLogRepo repository.AuditLogRepository) *AuditService {
return &AuditService{
auditLogRepo: auditLogRepo,
}
}
// Log 创建审计日志
func (s *AuditService) Log(ctx context.Context, workspaceID, userID string, action entity.AuditAction, resourceType entity.AuditResourceType, resourceID, resourceName string, details map[string]interface{}, ipAddress, userAgent string) error {
auditLog := &entity.AuditLog{
ID: uuid.New().String(),
WorkspaceID: workspaceID,
UserID: userID,
Action: action,
ResourceType: resourceType,
ResourceID: resourceID,
ResourceName: resourceName,
Details: details,
IPAddress: ipAddress,
UserAgent: userAgent,
CreatedAt: time.Now(),
}
return s.auditLogRepo.Create(ctx, auditLog)
}
// LogAction 简化版日志记录
func (s *AuditService) LogAction(ctx context.Context, workspaceID, userID string, action entity.AuditAction, resourceType entity.AuditResourceType, resourceName string) error {
return s.Log(ctx, workspaceID, userID, action, resourceType, "", resourceName, nil, "", "")
}
// LogWithDetails 带详情的日志记录
func (s *AuditService) LogWithDetails(ctx context.Context, workspaceID, userID string, action entity.AuditAction, resourceType entity.AuditResourceType, resourceID, resourceName string, details map[string]interface{}) error {
return s.Log(ctx, workspaceID, userID, action, resourceType, resourceID, resourceName, details, "", "")
}
// GetLogs 获取审计日志
func (s *AuditService) GetLogs(ctx context.Context, workspaceID string, limit int) ([]*entity.AuditLog, error) {
return s.auditLogRepo.GetByWorkspace(ctx, workspaceID, limit)
}
// GetUserLogs 获取用户的审计日志
func (s *AuditService) GetUserLogs(ctx context.Context, userID string, limit int) ([]*entity.AuditLog, error) {
return s.auditLogRepo.GetByUser(ctx, userID, limit)
}
// GetResourceLogs 获取资源的审计日志
func (s *AuditService) GetResourceLogs(ctx context.Context, resourceType entity.AuditResourceType, resourceID string, limit int) ([]*entity.AuditLog, error) {
return s.auditLogRepo.GetByResource(ctx, resourceType, resourceID, limit)
}
// GetAllLogs 获取所有审计日志Admin
func (s *AuditService) GetAllLogs(ctx context.Context, limit int, offset int) ([]*entity.AuditLog, error) {
return s.auditLogRepo.List(ctx, limit, offset)
}

View File

@ -22,9 +22,9 @@ type PasswordHasher interface {
// TokenGenerator Token 生成器接口
type TokenGenerator interface {
Generate(userID, username string) (accessToken, refreshToken string, err error)
Verify(token string) (userID, username string, err error)
VerifyWithIssuedAt(token string) (userID, username string, issuedAt int64, err error)
Generate(userID, username, role, workspaceID string) (accessToken, refreshToken string, err error)
Verify(token string) (userID, username, role, workspaceID string, err error)
VerifyWithIssuedAt(token string) (userID, username, role, workspaceID string, issuedAt int64, err error)
Refresh(refreshToken string) (newAccessToken string, err error)
}
@ -86,8 +86,8 @@ func (s *AuthService) Login(ctx context.Context, username, password string) (acc
return "", "", entity.ErrInvalidPassword
}
// 生成 Token
accessToken, refreshToken, err = s.tokenGenerator.Generate(user.ID, user.Username)
// 生成 Token (包含 role 和 workspace_id)
accessToken, refreshToken, err = s.tokenGenerator.Generate(user.ID, user.Username, string(user.Role), user.WorkspaceID)
if err != nil {
return "", "", err
}
@ -108,7 +108,7 @@ func (s *AuthService) GetUserByID(ctx context.Context, id string) (*entity.User,
// VerifyAccessToken 验证 Access Token包括 revoked_after 检查)
func (s *AuthService) VerifyAccessToken(ctx context.Context, token string) (userID, username string, err error) {
// 1. JWT 自验证
userID, username, issuedAt, err := s.tokenGenerator.VerifyWithIssuedAt(token)
userID, username, _, _, issuedAt, err := s.tokenGenerator.VerifyWithIssuedAt(token)
if err != nil {
return "", "", err
}

View File

@ -0,0 +1,137 @@
package service
import (
"context"
"errors"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
var (
ErrChartReferenceNotFound = errors.New("chart reference not found")
ErrChartReferenceExists = errors.New("chart reference already exists")
)
// ChartReferenceService Chart 引用领域服务
type ChartReferenceService struct {
chartRefRepo repository.ChartReferenceRepository
registryRepo repository.RegistryRepository
}
// NewChartReferenceService 创建 Chart 引用服务
func NewChartReferenceService(
chartRefRepo repository.ChartReferenceRepository,
registryRepo repository.RegistryRepository,
) *ChartReferenceService {
return &ChartReferenceService{
chartRefRepo: chartRefRepo,
registryRepo: registryRepo,
}
}
// Create 创建 Chart 引用
func (s *ChartReferenceService) Create(
ctx context.Context,
workspaceID, registryID, repository, chartName, description string,
) (*entity.ChartReference, error) {
// 检查 Registry 是否存在
registry, err := s.registryRepo.GetByID(ctx, registryID)
if err != nil {
return nil, errors.New("registry not found")
}
// 检查名称是否已存在
existing, _ := s.chartRefRepo.GetByName(ctx, workspaceID, chartName)
if existing != nil {
return nil, ErrChartReferenceExists
}
chartRef := entity.NewChartReference(workspaceID, registry.ID, repository, chartName, description)
chartRef.Description = description
if err := s.chartRefRepo.Create(ctx, chartRef); err != nil {
return nil, err
}
return chartRef, nil
}
// GetByID 获取 Chart 引用
func (s *ChartReferenceService) GetByID(ctx context.Context, id string) (*entity.ChartReference, error) {
chartRef, err := s.chartRefRepo.GetByID(ctx, id)
if err != nil {
return nil, ErrChartReferenceNotFound
}
return chartRef, nil
}
// GetByWorkspace 获取工作空间的所有 Chart 引用
func (s *ChartReferenceService) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.ChartReference, error) {
return s.chartRefRepo.GetByWorkspace(ctx, workspaceID)
}
// GetByRegistry 获取 Registry 的所有 Chart 引用
func (s *ChartReferenceService) GetByRegistry(ctx context.Context, registryID string) ([]*entity.ChartReference, error) {
return s.chartRefRepo.GetByRegistry(ctx, registryID)
}
// Update 更新 Chart 引用
func (s *ChartReferenceService) Update(
ctx context.Context,
id, registryID, repository, chartName, description string,
isEnabled bool,
) (*entity.ChartReference, error) {
chartRef, err := s.chartRefRepo.GetByID(ctx, id)
if err != nil {
return nil, ErrChartReferenceNotFound
}
if registryID != "" {
chartRef.RegistryID = registryID
}
if repository != "" {
chartRef.Repository = repository
}
if chartName != "" {
chartRef.ChartName = chartName
}
chartRef.Description = description
chartRef.IsEnabled = isEnabled
if err := s.chartRefRepo.Update(ctx, chartRef); err != nil {
return nil, err
}
return chartRef, nil
}
// Delete 删除 Chart 引用
func (s *ChartReferenceService) Delete(ctx context.Context, id string) error {
return s.chartRefRepo.Delete(ctx, id)
}
// List 列出所有 Chart 引用(管理员用)
func (s *ChartReferenceService) List(ctx context.Context) ([]*entity.ChartReference, error) {
return s.chartRefRepo.List(ctx)
}
// Enable 启用 Chart 引用
func (s *ChartReferenceService) Enable(ctx context.Context, id string) error {
chartRef, err := s.chartRefRepo.GetByID(ctx, id)
if err != nil {
return ErrChartReferenceNotFound
}
chartRef.IsEnabled = true
return s.chartRefRepo.Update(ctx, chartRef)
}
// Disable 禁用 Chart 引用
func (s *ChartReferenceService) Disable(ctx context.Context, id string) error {
chartRef, err := s.chartRefRepo.GetByID(ctx, id)
if err != nil {
return ErrChartReferenceNotFound
}
chartRef.IsEnabled = false
return s.chartRefRepo.Update(ctx, chartRef)
}

View File

@ -2,9 +2,16 @@ package service
import (
"context"
"encoding/base64"
"fmt"
"os"
"github.com/google/uuid"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)
// ClusterService 集群管理领域服务
@ -75,3 +82,105 @@ func (s *ClusterService) ListClusters(ctx context.Context) ([]*entity.Cluster, e
return s.clusterRepo.List(ctx)
}
// ListByWorkspace 列出指定 workspace 的集群(包括共享集群)
func (s *ClusterService) ListByWorkspace(ctx context.Context, workspaceID string) ([]*entity.Cluster, error) {
return s.clusterRepo.GetByWorkspace(ctx, workspaceID)
}
// GetSharedClusters 获取所有共享集群
func (s *ClusterService) GetSharedClusters(ctx context.Context) ([]*entity.Cluster, error) {
return s.clusterRepo.GetShared(ctx)
}
// TestConnection 测试集群连接是否可用
func (s *ClusterService) TestConnection(ctx context.Context, cluster *entity.Cluster) error {
// Mock 模式直接返回成功
if os.Getenv("ADAPTER_MODE") == "mock" {
return nil
}
// 尝试创建 k8s client
config, err := s.createRestConfig(cluster)
if err != nil {
return fmt.Errorf("failed to create k8s config: %w", err)
}
// 设置超时
config.Timeout = 30 * 1000000000 // 30秒 (nanoseconds)
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return fmt.Errorf("failed to create k8s client: %w", err)
}
// 测试连接 - 获取 version 信息
version, err := clientset.ServerVersion()
if err != nil {
return fmt.Errorf("failed to connect to cluster: %w", err)
}
if version == nil {
return fmt.Errorf("cluster returned nil version")
}
return nil
}
// createRestConfig 从 cluster 实体创建 k8s REST 配置
func (s *ClusterService) createRestConfig(cluster *entity.Cluster) (*rest.Config, error) {
// 优先使用 kubeconfig 格式(如果 CAData 包含完整的 kubeconfig 内容)
if len(cluster.CAData) > 100 && (cluster.CAData[:11] == "apiVersion:" || cluster.CAData[:5] == "kind:") {
return clientcmd.RESTConfigFromKubeConfig([]byte(cluster.CAData))
}
// 使用证书或 token 认证
config := &rest.Config{
Host: cluster.Host,
}
if cluster.CertData != "" && cluster.KeyData != "" {
// 尝试解码 base64 编码的证书,如果失败则尝试原始 PEM
var caData, certData, keyData []byte
var decodeErr error
// 先尝试 base64 解码
caData, decodeErr = base64.StdEncoding.DecodeString(cluster.CAData)
if decodeErr != nil {
// base64 解码失败,可能是原始 PEM
caData = []byte(cluster.CAData)
}
certData, decodeErr = base64.StdEncoding.DecodeString(cluster.CertData)
if decodeErr != nil {
certData = []byte(cluster.CertData)
}
keyData, decodeErr = base64.StdEncoding.DecodeString(cluster.KeyData)
if decodeErr != nil {
keyData = []byte(cluster.KeyData)
}
config.TLSClientConfig = rest.TLSClientConfig{
CAData: caData,
CertData: certData,
KeyData: keyData,
Insecure: false,
}
} else if cluster.Token != "" {
config.BearerToken = cluster.Token
} else {
// 尝试使用本地 kubeconfig
kubeconfig := os.Getenv("KUBECONFIG")
if kubeconfig == "" {
kubeconfig = ".kube/config"
}
// 尝试从文件加载 kubeconfig
if _, err := os.Stat(kubeconfig); err == nil {
return clientcmd.BuildConfigFromFlags("", kubeconfig)
}
return clientcmd.BuildConfigFromFlags("", kubeconfig)
}
return config, nil
}

View File

@ -6,6 +6,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/google/uuid"
@ -336,9 +337,17 @@ func (s *InstanceService) executeAndSyncRollback(ctx context.Context, instanceID
// executeAndSyncUninstall 异步执行卸载并监控状态
func (s *InstanceService) executeAndSyncUninstall(ctx context.Context, instanceID string, cluster *entity.Cluster, releaseName, namespace string) {
// 先验证 release 名称是否有效
// 如果名称无效,说明这个 release 根本不可能存在于 Helm 中,直接删除数据库记录
if err := entity.ValidateReleaseName(releaseName); err != nil {
// Release 名称无效,直接删除数据库记录
_ = s.instanceRepo.Delete(ctx, instanceID)
return
}
// 执行 Helm 卸载
err := s.helmClient.Uninstall(ctx, cluster, releaseName, namespace)
// 获取实例
instance, getErr := s.instanceRepo.GetByID(ctx, instanceID)
if getErr != nil {
@ -346,13 +355,22 @@ func (s *InstanceService) executeAndSyncUninstall(ctx context.Context, instanceI
}
if err != nil {
// 如果错误不是"未找到",则标记为失败
if !errors.Is(err, entity.ErrInstanceNotFound) {
instance.MarkFailure("Helm uninstall failed", err)
_ = s.instanceRepo.Update(ctx, instance)
} else {
// 如果未找到,说明已经卸载,直接删除数据库记录
// 检查错误类型
if errors.Is(err, entity.ErrInstanceNotFound) {
// 未找到,说明已经卸载,直接删除数据库记录
_ = s.instanceRepo.Delete(ctx, instanceID)
} else {
// 检查是否是 release 名称无效的错误(可能在某些情况下 Helm 会返回这个错误)
errMsg := strings.ToLower(err.Error())
if strings.Contains(errMsg, "release name is invalid") ||
(strings.Contains(errMsg, "invalid") && strings.Contains(errMsg, "release")) {
// Release 名称无效,直接删除数据库记录
_ = s.instanceRepo.Delete(ctx, instanceID)
} else {
// 其他错误,标记为失败
instance.MarkFailure("Helm uninstall failed", err)
_ = s.instanceRepo.Update(ctx, instance)
}
}
return
}
@ -360,7 +378,7 @@ func (s *InstanceService) executeAndSyncUninstall(ctx context.Context, instanceI
// 卸载成功,标记为已卸载
instance.MarkSuccess(entity.StatusUninstalled, instance.Revision, "Instance uninstalled successfully")
_ = s.instanceRepo.Update(ctx, instance)
// 验证卸载是否完成:尝试获取状态,如果获取不到说明已卸载
time.Sleep(3 * time.Second)
_, statusErr := s.helmClient.GetStatus(ctx, cluster, releaseName, namespace)

View File

@ -0,0 +1,224 @@
package service
import (
"context"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
// QuotaService 配额领域服务
type QuotaService struct {
quotaRepo repository.QuotaRepository
instanceRepo repository.InstanceRepository
workspaceRepo repository.WorkspaceRepository
}
// NewQuotaService 创建配额服务
func NewQuotaService(
quotaRepo repository.QuotaRepository,
instanceRepo repository.InstanceRepository,
workspaceRepo repository.WorkspaceRepository,
) *QuotaService {
return &QuotaService{
quotaRepo: quotaRepo,
instanceRepo: instanceRepo,
workspaceRepo: workspaceRepo,
}
}
// CheckQuota 检查配额是否足够
func (s *QuotaService) CheckQuota(ctx context.Context, workspaceID string, cpu, gpu, gpuMemory float64) error {
// 检查 CPU 配额
if cpu > 0 {
quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, entity.ResourceCPU)
if err != nil {
return err
}
if quota != nil && !quota.CanAllocate(cpu) {
return entity.ErrQuotaExceeded
}
}
// 检查 GPU 配额
if gpu > 0 {
quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, entity.ResourceGPU)
if err != nil {
return err
}
if quota != nil && !quota.CanAllocate(gpu) {
return entity.ErrQuotaExceeded
}
}
// 检查 GPU Memory 配额
if gpuMemory > 0 {
quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, entity.ResourceGPUMemory)
if err != nil {
return err
}
if quota != nil && !quota.CanAllocate(gpuMemory) {
return entity.ErrQuotaExceeded
}
}
return nil
}
// AllocateQuota 分配配额(部署实例成功后调用)
func (s *QuotaService) AllocateQuota(ctx context.Context, workspaceID string, cpu, gpu, gpuMemory float64) error {
// 分配 CPU
if cpu > 0 {
quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, entity.ResourceCPU)
if err != nil {
return err
}
if quota != nil {
quota.Allocate(cpu)
if err := s.quotaRepo.Update(ctx, quota); err != nil {
return err
}
}
}
// 分配 GPU
if gpu > 0 {
quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, entity.ResourceGPU)
if err != nil {
return err
}
if quota != nil {
quota.Allocate(gpu)
if err := s.quotaRepo.Update(ctx, quota); err != nil {
return err
}
}
}
// 分配 GPU Memory
if gpuMemory > 0 {
quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, entity.ResourceGPUMemory)
if err != nil {
return err
}
if quota != nil {
quota.Allocate(gpuMemory)
if err := s.quotaRepo.Update(ctx, quota); err != nil {
return err
}
}
}
return nil
}
// ReleaseQuota 释放配额(删除实例后调用)
func (s *QuotaService) ReleaseQuota(ctx context.Context, workspaceID string, cpu, gpu, gpuMemory float64) error {
// 释放 CPU
if cpu > 0 {
quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, entity.ResourceCPU)
if err != nil {
return err
}
if quota != nil {
quota.Release(cpu)
if err := s.quotaRepo.Update(ctx, quota); err != nil {
return err
}
}
}
// 释放 GPU
if gpu > 0 {
quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, entity.ResourceGPU)
if err != nil {
return err
}
if quota != nil {
quota.Release(gpu)
if err := s.quotaRepo.Update(ctx, quota); err != nil {
return err
}
}
}
// 释放 GPU Memory
if gpuMemory > 0 {
quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, entity.ResourceGPUMemory)
if err != nil {
return err
}
if quota != nil {
quota.Release(gpuMemory)
if err := s.quotaRepo.Update(ctx, quota); err != nil {
return err
}
}
}
return nil
}
// GetQuotaUsage 获取配额使用情况
func (s *QuotaService) GetQuotaUsage(ctx context.Context, workspaceID string) (map[entity.ResourceType]*entity.WorkspaceQuota, error) {
quotas, err := s.quotaRepo.GetByWorkspace(ctx, workspaceID)
if err != nil {
return nil, err
}
result := make(map[entity.ResourceType]*entity.WorkspaceQuota)
for _, q := range quotas {
result[q.ResourceType] = q
}
// 确保所有资源类型都有返回值
for _, rt := range []entity.ResourceType{entity.ResourceCPU, entity.ResourceGPU, entity.ResourceGPUMemory} {
if _, ok := result[rt]; !ok {
result[rt] = &entity.WorkspaceQuota{
WorkspaceID: workspaceID,
ResourceType: rt,
HardLimit: 0,
SoftLimit: 0,
Used: 0,
}
}
}
return result, nil
}
// RecalculateQuota 重新计算配额使用量(从实例汇总)
func (s *QuotaService) RecalculateQuota(ctx context.Context, workspaceID string) error {
// 获取 workspace 的所有实例
instances, err := s.instanceRepo.GetByWorkspace(ctx, workspaceID)
if err != nil {
return err
}
// 汇总资源使用
var totalCPU, totalGPU, totalGPUMemory float64
for _, inst := range instances {
totalCPU += inst.CPURequested
totalGPU += inst.GPURequested
// GPU Memory 需要解析字符串
// 这里简化处理,实际需要解析 "16Gi" 这样的格式
}
// 更新配额
resources := []entity.ResourceType{entity.ResourceCPU, entity.ResourceGPU, entity.ResourceGPUMemory}
values := []float64{totalCPU, totalGPU, totalGPUMemory}
for i, rt := range resources {
quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, rt)
if err != nil {
return err
}
if quota != nil {
quota.Used = values[i]
if err := s.quotaRepo.Update(ctx, quota); err != nil {
return err
}
}
}
return nil
}

View File

@ -2,6 +2,8 @@ package service
import (
"context"
"os"
"github.com/google/uuid"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
@ -40,6 +42,13 @@ func (s *RegistryService) CreateRegistry(ctx context.Context, registry *entity.R
return entity.ErrRegistryExists
}
// 非 mock 模式下验证连接
if os.Getenv("ADAPTER_MODE") != "mock" {
if err := s.ociClient.CheckHealth(ctx, registry); err != nil {
return err
}
}
return s.registryRepo.Create(ctx, registry)
}

View File

@ -0,0 +1,116 @@
package service
import (
"context"
"errors"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
var (
ErrStorageNotFound = errors.New("storage backend not found")
ErrStorageExists = errors.New("storage backend already exists")
)
// StorageService 存储后端领域服务
type StorageService struct {
storageRepo repository.StorageRepository
}
// NewStorageService 创建存储后端服务
func NewStorageService(storageRepo repository.StorageRepository) *StorageService {
return &StorageService{
storageRepo: storageRepo,
}
}
// Create 创建存储后端
func (s *StorageService) Create(
ctx context.Context,
workspaceID, ownerID, name string,
storageType entity.StorageType,
config entity.StorageConfig,
description string,
isDefault, isShared bool,
) (*entity.StorageBackend, error) {
// 检查名称是否已存在
existing, _ := s.storageRepo.GetByName(ctx, workspaceID, name)
if existing != nil {
return nil, ErrStorageExists
}
storage := entity.NewStorageBackend(workspaceID, ownerID, name, storageType, config)
storage.Description = description
storage.IsDefault = isDefault
storage.IsShared = isShared
if err := s.storageRepo.Create(ctx, storage); err != nil {
return nil, err
}
return storage, nil
}
// GetByID 获取存储后端
func (s *StorageService) GetByID(ctx context.Context, id string) (*entity.StorageBackend, error) {
storage, err := s.storageRepo.GetByID(ctx, id)
if err != nil {
return nil, ErrStorageNotFound
}
return storage, nil
}
// GetByWorkspace 获取工作空间的所有存储后端
func (s *StorageService) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.StorageBackend, error) {
return s.storageRepo.GetByWorkspace(ctx, workspaceID)
}
// GetShared 获取所有共享存储后端
func (s *StorageService) GetShared(ctx context.Context) ([]*entity.StorageBackend, error) {
return s.storageRepo.GetShared(ctx)
}
// GetDefault 获取工作空间的默认存储后端
func (s *StorageService) GetDefault(ctx context.Context, workspaceID string) (*entity.StorageBackend, error) {
return s.storageRepo.GetDefault(ctx, workspaceID)
}
// Update 更新存储后端
func (s *StorageService) Update(
ctx context.Context,
id, name, description string,
storageType entity.StorageType,
config entity.StorageConfig,
isDefault, isShared bool,
) (*entity.StorageBackend, error) {
storage, err := s.storageRepo.GetByID(ctx, id)
if err != nil {
return nil, ErrStorageNotFound
}
if name != "" {
storage.Name = name
}
storage.Description = description
storage.Type = storageType
storage.Config = config
storage.IsDefault = isDefault
storage.IsShared = isShared
if err := s.storageRepo.Update(ctx, storage); err != nil {
return nil, err
}
return storage, nil
}
// Delete 删除存储后端
func (s *StorageService) Delete(ctx context.Context, id string) error {
return s.storageRepo.Delete(ctx, id)
}
// List 列出所有存储后端(管理员用)
func (s *StorageService) List(ctx context.Context) ([]*entity.StorageBackend, error) {
return s.storageRepo.List(ctx)
}

View File

@ -0,0 +1,298 @@
package service
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/google/uuid"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
// UserManagementService 用户管理领域服务(仅 Admin 可用)
type UserManagementService struct {
userRepo repository.UserRepository
workspaceRepo repository.WorkspaceRepository
passwordHasher PasswordHasher
}
// NewUserManagementService 创建用户管理服务
func NewUserManagementService(
userRepo repository.UserRepository,
workspaceRepo repository.WorkspaceRepository,
passwordHasher PasswordHasher,
) *UserManagementService {
return &UserManagementService{
userRepo: userRepo,
workspaceRepo: workspaceRepo,
passwordHasher: passwordHasher,
}
}
// CreateUser 创建用户Admin 操作)
func (s *UserManagementService) CreateUser(ctx context.Context, username, password, email, role string, workspaceID string) (*entity.User, error) {
// 检查用户是否已存在
existing, _ := s.userRepo.GetByUsername(ctx, username)
if existing != nil {
return nil, entity.ErrUserExists
}
// 验证角色
if role != string(entity.RoleAdmin) && role != string(entity.RoleUser) {
return nil, fmt.Errorf("invalid role: %s", role)
}
// 如果指定了 workspace验证 workspace 存在
if workspaceID != "" {
_, err := s.workspaceRepo.GetByID(ctx, workspaceID)
if err != nil {
if err == entity.ErrWorkspaceNotFound {
return nil, entity.ErrWorkspaceNotFound
}
return nil, err
}
}
// Admin 不能分配到 workspace
if role == string(entity.RoleAdmin) && workspaceID != "" {
workspaceID = ""
}
// 哈希密码
passwordHash, err := s.passwordHasher.Hash(password)
if err != nil {
return nil, err
}
// 生成占位邮箱
if email == "" {
email = username + "@local.ocdp"
}
// 创建用户
user := entity.NewUser(username, passwordHash, email)
user.ID = uuid.New().String()
user.Role = entity.UserRole(role)
user.WorkspaceID = workspaceID
user.IsActive = true
user.MustChangePassword = true // 首次登录必须修改密码
if err := user.Validate(); err != nil {
return nil, err
}
if err := s.userRepo.Create(ctx, user); err != nil {
return nil, err
}
return user, nil
}
// GetUser 获取用户
func (s *UserManagementService) GetUser(ctx context.Context, id string) (*entity.User, error) {
return s.userRepo.GetByID(ctx, id)
}
// ListUsers 列出用户(可筛选 workspace
func (s *UserManagementService) ListUsers(ctx context.Context, workspaceID string) ([]*entity.User, error) {
if workspaceID != "" {
return s.userRepo.ListByWorkspace(ctx, workspaceID)
}
return s.userRepo.List(ctx)
}
// UpdateUser 更新用户信息
func (s *UserManagementService) UpdateUser(ctx context.Context, user *entity.User) error {
return s.userRepo.Update(ctx, user)
}
// SetUserActive 启用/禁用用户
func (s *UserManagementService) SetUserActive(ctx context.Context, userID string, isActive bool) error {
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return err
}
user.IsActive = isActive
return s.userRepo.Update(ctx, user)
}
// ChangeUserWorkspace 分配用户到 workspace
func (s *UserManagementService) ChangeUserWorkspace(ctx context.Context, userID, workspaceID string) error {
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return err
}
// Admin 不能分配到 workspace
if user.Role == entity.RoleAdmin {
return fmt.Errorf("admin user cannot be assigned to workspace")
}
// 验证 workspace 存在
if workspaceID != "" {
_, err := s.workspaceRepo.GetByID(ctx, workspaceID)
if err != nil {
if err == sql.ErrNoRows || err == entity.ErrWorkspaceNotFound {
return entity.ErrWorkspaceNotFound
}
return err
}
}
user.WorkspaceID = workspaceID
return s.userRepo.Update(ctx, user)
}
// ResetPassword 重置用户密码Admin 操作)
func (s *UserManagementService) ResetPassword(ctx context.Context, userID, newPassword string) error {
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return err
}
// 哈希新密码
passwordHash, err := s.passwordHasher.Hash(newPassword)
if err != nil {
return err
}
// 更新密码并设置必须修改密码标志
user.PasswordHash = passwordHash
user.MustChangePassword = true
user.RevokeAllTokens() // 强制登出所有会话
return s.userRepo.Update(ctx, user)
}
// DeleteUser 删除用户
func (s *UserManagementService) DeleteUser(ctx context.Context, id string) error {
return s.userRepo.Delete(ctx, id)
}
// GetUserWithWorkspace 获取用户及其 workspace 信息
type UserWithWorkspace struct {
User *entity.User
Workspace *entity.Workspace
}
// GetUserWithWorkspace 获取用户及其 workspace 信息
func (s *UserManagementService) GetUserWithWorkspace(ctx context.Context, userID string) (*UserWithWorkspace, error) {
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return nil, err
}
result := &UserWithWorkspace{
User: user,
}
if user.WorkspaceID != "" {
workspace, _ := s.workspaceRepo.GetByID(ctx, user.WorkspaceID)
result.Workspace = workspace
}
return result, nil
}
// ListUsersWithWorkspace 列出用户及其 workspace 信息
func (s *UserManagementService) ListUsersWithWorkspace(ctx context.Context) ([]*UserWithWorkspace, error) {
users, err := s.userRepo.List(ctx)
if err != nil {
return nil, err
}
// 预加载所有 workspace
workspaces, err := s.workspaceRepo.List(ctx)
if err != nil {
return nil, err
}
workspaceMap := make(map[string]*entity.Workspace)
for _, w := range workspaces {
workspaceMap[w.ID] = w
}
result := make([]*UserWithWorkspace, len(users))
for i, user := range users {
result[i] = &UserWithWorkspace{
User: user,
}
if user.WorkspaceID != "" {
result[i].Workspace = workspaceMap[user.WorkspaceID]
}
}
return result, nil
}
// EnsureAdminExists 确保存在一个 Admin 用户
func (s *UserManagementService) EnsureAdminExists(ctx context.Context, defaultPassword string) error {
users, err := s.userRepo.List(ctx)
if err != nil {
return err
}
// 检查是否已有 admin
for _, u := range users {
if u.Role == entity.RoleAdmin {
return nil
}
}
// 创建默认 admin 用户
_, err = s.CreateUser(ctx, "admin", defaultPassword, "", string(entity.RoleAdmin), "")
return err
}
// CreateInitialUser 创建初始用户(首次启动时调用)
func (s *UserManagementService) CreateInitialUser(ctx context.Context, username, password, role string) (*entity.User, error) {
// 检查是否已有用户
users, err := s.userRepo.List(ctx)
if err != nil {
return nil, err
}
if len(users) > 0 {
return nil, fmt.Errorf("initial user already exists")
}
// 验证角色
if role != string(entity.RoleAdmin) && role != string(entity.RoleUser) {
return nil, fmt.Errorf("invalid role: %s", role)
}
// 哈希密码
passwordHash, err := s.passwordHasher.Hash(password)
if err != nil {
return nil, err
}
// 生成占位邮箱
email := username + "@local.ocdp"
// 创建用户
user := entity.NewUser(username, passwordHash, email)
user.ID = uuid.New().String()
user.Role = entity.UserRole(role)
// workspace_id 为 NULLadmin或空首个普通用户
user.IsActive = true
user.MustChangePassword = false // 初始用户不需要强制修改密码
if err := user.Validate(); err != nil {
return nil, err
}
// 设置创建时间和更新时间
now := time.Now()
user.CreatedAt = now
user.UpdatedAt = now
if err := s.userRepo.Create(ctx, user); err != nil {
return nil, err
}
return user, nil
}

View File

@ -0,0 +1,143 @@
package service
import (
"context"
"errors"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
var (
ErrTemplateNotFound = errors.New("template not found")
ErrTemplateExists = errors.New("template already exists")
)
// ValuesTemplateService Values 模板领域服务
type ValuesTemplateService struct {
valuesTemplateRepo repository.ValuesTemplateRepository
chartRefRepo repository.ChartReferenceRepository
}
// NewValuesTemplateService 创建 Values 模板服务
func NewValuesTemplateService(
valuesTemplateRepo repository.ValuesTemplateRepository,
chartRefRepo repository.ChartReferenceRepository,
) *ValuesTemplateService {
return &ValuesTemplateService{
valuesTemplateRepo: valuesTemplateRepo,
chartRefRepo: chartRefRepo,
}
}
// Create 创建 Values 模板
func (s *ValuesTemplateService) Create(
ctx context.Context,
workspaceID, ownerID, chartRefID, name, description, valuesYAML string,
isDefault bool,
) (*entity.ValuesTemplate, error) {
// 检查 Chart Reference 是否存在
chartRef, err := s.chartRefRepo.GetByID(ctx, chartRefID)
if err != nil {
return nil, errors.New("chart reference not found")
}
// 检查名称是否已存在
existing, _ := s.valuesTemplateRepo.GetByName(ctx, workspaceID, chartRefID, name)
if existing != nil {
return nil, ErrTemplateExists
}
template := entity.NewValuesTemplate(workspaceID, ownerID, chartRef.ID, name, valuesYAML)
template.Description = description
template.IsDefault = isDefault
if err := s.valuesTemplateRepo.Create(ctx, template); err != nil {
return nil, err
}
return template, nil
}
// GetByID 获取 Values 模板
func (s *ValuesTemplateService) GetByID(ctx context.Context, id string) (*entity.ValuesTemplate, error) {
template, err := s.valuesTemplateRepo.GetByID(ctx, id)
if err != nil {
return nil, ErrTemplateNotFound
}
return template, nil
}
// GetByWorkspace 获取工作空间的所有 Values 模板
func (s *ValuesTemplateService) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.ValuesTemplate, error) {
return s.valuesTemplateRepo.GetByWorkspace(ctx, workspaceID)
}
// GetByChartReference 获取 Chart Reference 的所有 Values 模板
func (s *ValuesTemplateService) GetByChartReference(ctx context.Context, chartRefID string) ([]*entity.ValuesTemplate, error) {
return s.valuesTemplateRepo.GetByChartReference(ctx, chartRefID)
}
// GetHistory 获取模板的版本历史
func (s *ValuesTemplateService) GetHistory(ctx context.Context, chartRefID, name string) ([]*entity.ValuesTemplate, error) {
return s.valuesTemplateRepo.GetHistory(ctx, chartRefID, name)
}
// Update 更新 Values 模板(创建新版本)
func (s *ValuesTemplateService) Update(
ctx context.Context,
id, description, valuesYAML string,
isDefault bool,
) (*entity.ValuesTemplate, error) {
template, err := s.valuesTemplateRepo.GetByID(ctx, id)
if err != nil {
return nil, ErrTemplateNotFound
}
template.Description = description
template.ValuesYAML = valuesYAML
template.IsDefault = isDefault
if err := s.valuesTemplateRepo.Update(ctx, template); err != nil {
return nil, err
}
// 获取最新版本
return s.valuesTemplateRepo.GetByName(ctx, template.WorkspaceID, template.ChartReferenceID, template.Name)
}
// Delete 删除 Values 模板
func (s *ValuesTemplateService) Delete(ctx context.Context, id string) error {
return s.valuesTemplateRepo.Delete(ctx, id)
}
// List 列出所有 Values 模板(管理员用)
func (s *ValuesTemplateService) List(ctx context.Context) ([]*entity.ValuesTemplate, error) {
return s.valuesTemplateRepo.List(ctx)
}
// Rollback 回滚到指定版本
func (s *ValuesTemplateService) Rollback(ctx context.Context, templateID string) (*entity.ValuesTemplate, error) {
// 获取历史版本模板
oldTemplate, err := s.valuesTemplateRepo.GetByID(ctx, templateID)
if err != nil {
return nil, ErrTemplateNotFound
}
// 重新创建该版本(创建新版本,内容与旧版本相同)
newTemplate := &entity.ValuesTemplate{
WorkspaceID: oldTemplate.WorkspaceID,
OwnerID: oldTemplate.OwnerID,
ChartReferenceID: oldTemplate.ChartReferenceID,
Name: oldTemplate.Name,
Description: oldTemplate.Description,
ValuesYAML: oldTemplate.ValuesYAML,
}
if err := s.valuesTemplateRepo.Update(ctx, newTemplate); err != nil {
return nil, err
}
// 获取最新版本
return s.valuesTemplateRepo.GetByName(ctx, newTemplate.WorkspaceID, newTemplate.ChartReferenceID, newTemplate.Name)
}

View File

@ -0,0 +1,121 @@
package service
import (
"context"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
// WorkspaceService 工作空间领域服务
type WorkspaceService struct {
workspaceRepo repository.WorkspaceRepository
quotaRepo repository.QuotaRepository
userRepo repository.UserRepository
}
// NewWorkspaceService 创建工作空间服务
func NewWorkspaceService(
workspaceRepo repository.WorkspaceRepository,
quotaRepo repository.QuotaRepository,
userRepo repository.UserRepository,
) *WorkspaceService {
return &WorkspaceService{
workspaceRepo: workspaceRepo,
quotaRepo: quotaRepo,
userRepo: userRepo,
}
}
// Create 创建工作空间
func (s *WorkspaceService) Create(ctx context.Context, name, description, createdBy string) (*entity.Workspace, error) {
// 检查名称是否已存在
existing, _ := s.workspaceRepo.GetByName(ctx, name)
if existing != nil {
return nil, entity.ErrWorkspaceExists
}
workspace := entity.NewWorkspace(name, description, createdBy)
if err := s.workspaceRepo.Create(ctx, workspace); err != nil {
return nil, err
}
return workspace, nil
}
// GetByID 获取工作空间
func (s *WorkspaceService) GetByID(ctx context.Context, id string) (*entity.Workspace, error) {
return s.workspaceRepo.GetByID(ctx, id)
}
// GetByName 获取工作空间
func (s *WorkspaceService) GetByName(ctx context.Context, name string) (*entity.Workspace, error) {
return s.workspaceRepo.GetByName(ctx, name)
}
// Update 更新工作空间
func (s *WorkspaceService) Update(ctx context.Context, workspace *entity.Workspace) error {
return s.workspaceRepo.Update(ctx, workspace)
}
// Delete 删除工作空间
func (s *WorkspaceService) Delete(ctx context.Context, id string) error {
// 删除关联的配额
if err := s.quotaRepo.DeleteByWorkspace(ctx, id); err != nil {
return err
}
return s.workspaceRepo.Delete(ctx, id)
}
// List 列出所有工作空间
func (s *WorkspaceService) List(ctx context.Context) ([]*entity.Workspace, error) {
return s.workspaceRepo.List(ctx)
}
// GetQuotas 获取工作空间配额
func (s *WorkspaceService) GetQuotas(ctx context.Context, workspaceID string) ([]*entity.WorkspaceQuota, error) {
return s.quotaRepo.GetByWorkspace(ctx, workspaceID)
}
// SetQuota 设置配额
func (s *WorkspaceService) SetQuota(ctx context.Context, workspaceID string, resourceType entity.ResourceType, hardLimit, softLimit float64) (*entity.WorkspaceQuota, error) {
quota := entity.NewWorkspaceQuota(workspaceID, resourceType, hardLimit, softLimit)
if err := s.quotaRepo.Create(ctx, quota); err != nil {
return nil, err
}
return quota, nil
}
// SetQuotas 批量设置配额
func (s *WorkspaceService) SetQuotas(ctx context.Context, workspaceID string, quotas map[entity.ResourceType]struct {
HardLimit float64
SoftLimit float64
}) error {
for resourceType, config := range quotas {
quota := entity.NewWorkspaceQuota(workspaceID, resourceType, config.HardLimit, config.SoftLimit)
if err := s.quotaRepo.Create(ctx, quota); err != nil {
return err
}
}
return nil
}
// GetOrCreateDefaultQuota 获取或创建默认配额
func (s *WorkspaceService) GetOrCreateDefaultQuota(ctx context.Context, workspaceID string, resourceType entity.ResourceType) (*entity.WorkspaceQuota, error) {
quota, _ := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, resourceType)
if quota != nil {
return quota, nil
}
// 创建默认配额(无限制)
quota = entity.NewWorkspaceQuota(workspaceID, resourceType, 0, 0)
if err := s.quotaRepo.Create(ctx, quota); err != nil {
return nil, err
}
return quota, nil
}
// GetUsers 获取工作空间的用户
func (s *WorkspaceService) GetUsers(ctx context.Context, workspaceID string) ([]*entity.User, error) {
return s.userRepo.ListByWorkspace(ctx, workspaceID)
}

View File

@ -26,98 +26,106 @@ func NewJWTManager(secretKey string) *JWTManager {
// Claims JWT Claims
type Claims struct {
UserID string `json:"user_id"`
Username string `json:"username"`
UserID string `json:"user_id"`
Username string `json:"username"`
Role string `json:"role"`
WorkspaceID string `json:"workspace_id"`
jwt.RegisteredClaims
}
// Generate 生成 Access Token 和 Refresh Token
func (m *JWTManager) Generate(userID, username string) (accessToken, refreshToken string, err error) {
func (m *JWTManager) Generate(userID, username, role, workspaceID string) (accessToken, refreshToken string, err error) {
// 生成 Access Token
accessClaims := &Claims{
UserID: userID,
Username: username,
UserID: userID,
Username: username,
Role: role,
WorkspaceID: workspaceID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(AccessTokenDuration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
accessTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
accessToken, err = accessTokenObj.SignedString([]byte(m.secretKey))
if err != nil {
return "", "", fmt.Errorf("failed to sign access token: %w", err)
}
// 生成 Refresh Token
refreshClaims := &Claims{
UserID: userID,
Username: username,
UserID: userID,
Username: username,
Role: role,
WorkspaceID: workspaceID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(RefreshTokenDuration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
refreshTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
refreshToken, err = refreshTokenObj.SignedString([]byte(m.secretKey))
if err != nil {
return "", "", fmt.Errorf("failed to sign refresh token: %w", err)
}
return accessToken, refreshToken, nil
}
// Verify 验证 Token
func (m *JWTManager) Verify(tokenString string) (userID, username string, err error) {
userID, username, _, err = m.VerifyWithIssuedAt(tokenString)
return userID, username, err
func (m *JWTManager) Verify(tokenString string) (userID, username, role, workspaceID string, err error) {
userID, username, role, workspaceID, _, err = m.VerifyWithIssuedAt(tokenString)
return userID, username, role, workspaceID, err
}
// VerifyWithIssuedAt 验证 Token 并返回签发时间
func (m *JWTManager) VerifyWithIssuedAt(tokenString string) (userID, username string, issuedAt int64, err error) {
func (m *JWTManager) VerifyWithIssuedAt(tokenString string) (userID, username, role, workspaceID string, issuedAt int64, err error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(m.secretKey), nil
})
if err != nil {
return "", "", 0, fmt.Errorf("failed to parse token: %w", err)
return "", "", "", "", 0, fmt.Errorf("failed to parse token: %w", err)
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims.UserID, claims.Username, claims.IssuedAt.Unix(), nil
return claims.UserID, claims.Username, claims.Role, claims.WorkspaceID, claims.IssuedAt.Unix(), nil
}
return "", "", 0, fmt.Errorf("invalid token")
return "", "", "", "", 0, fmt.Errorf("invalid token")
}
// Refresh 刷新 Token
func (m *JWTManager) Refresh(refreshToken string) (string, error) {
// 验证 Refresh Token
userID, username, err := m.Verify(refreshToken)
userID, username, role, workspaceID, err := m.Verify(refreshToken)
if err != nil {
return "", fmt.Errorf("invalid refresh token: %w", err)
}
// 生成新的 Access Token
accessClaims := &Claims{
UserID: userID,
Username: username,
UserID: userID,
Username: username,
Role: role,
WorkspaceID: workspaceID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(AccessTokenDuration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
accessTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
newAccessToken, err := accessTokenObj.SignedString([]byte(m.secretKey))
if err != nil {
return "", fmt.Errorf("failed to sign new access token: %w", err)
}
return newAccessToken, nil
}