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:
306
backend/internal/adapter/input/http/rest/workspace_handler.go
Normal file
306
backend/internal/adapter/input/http/rest/workspace_handler.go
Normal file
@ -0,0 +1,306 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/ocdp/cluster-service/internal/adapter/input/http/dto"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/service"
|
||||
)
|
||||
|
||||
// WorkspaceHandler 工作空间 HTTP 处理程序
|
||||
type WorkspaceHandler struct {
|
||||
workspaceService *service.WorkspaceService
|
||||
authService *service.AuthService
|
||||
}
|
||||
|
||||
// NewWorkspaceHandler 创建工作空间处理程序
|
||||
func NewWorkspaceHandler(workspaceService *service.WorkspaceService, authService *service.AuthService) *WorkspaceHandler {
|
||||
return &WorkspaceHandler{
|
||||
workspaceService: workspaceService,
|
||||
authService: authService,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateWorkspace 创建工作空间
|
||||
// @Summary 创建工作空间
|
||||
// @Description 创建新的工作空间(Admin 专用)
|
||||
// @Tags workspace
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.CreateWorkspaceRequest true "创建工作空间请求"
|
||||
// @Success 200 {object} dto.WorkspaceDTO
|
||||
// @Router /workspaces [post]
|
||||
func (h *WorkspaceHandler) CreateWorkspace(w http.ResponseWriter, r *http.Request) {
|
||||
// 检查权限(Admin)
|
||||
if !h.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.CreateWorkspaceRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body", "")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取创建者 ID
|
||||
userID := GetUserIDFromRequest(r)
|
||||
|
||||
workspace, err := h.workspaceService.Create(r.Context(), req.Name, req.Description, userID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, err.Error(), "")
|
||||
return
|
||||
}
|
||||
|
||||
respondSuccess(w, "", dto.WorkspaceDTOFromEntity(workspace))
|
||||
}
|
||||
|
||||
// GetWorkspace 获取工作空间
|
||||
// @Summary 获取工作空间
|
||||
// @Description 获取指定工作空间的详细信息和配额
|
||||
// @Tags workspace
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param workspace_id path string true "工作空间 ID"
|
||||
// @Success 200 {object} dto.WorkspaceResponse
|
||||
// @Router /workspaces/{workspace_id} [get]
|
||||
func (h *WorkspaceHandler) GetWorkspace(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
workspaceID := vars["workspace_id"]
|
||||
|
||||
workspace, err := h.workspaceService.GetByID(r.Context(), workspaceID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusNotFound, "Workspace not found", "")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查访问权限
|
||||
if !h.canAccessWorkspace(w, r, workspace.ID) {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取配额
|
||||
quotas, _ := h.workspaceService.GetQuotas(r.Context(), workspace.ID)
|
||||
|
||||
response := dto.WorkspaceResponse{
|
||||
Workspace: dto.WorkspaceDTOFromEntity(workspace),
|
||||
Quotas: dto.QuotaDTOsFromEntities(quotas),
|
||||
}
|
||||
|
||||
respondSuccess(w, "", response)
|
||||
}
|
||||
|
||||
// UpdateWorkspace 更新工作空间
|
||||
// @Summary 更新工作空间
|
||||
// @Description 更新工作空间信息(Admin 专用)
|
||||
// @Tags workspace
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param workspace_id path string true "工作空间 ID"
|
||||
// @Param request body dto.UpdateWorkspaceRequest true "更新工作空间请求"
|
||||
// @Success 200 {object} dto.WorkspaceDTO
|
||||
// @Router /workspaces/{workspace_id} [put]
|
||||
func (h *WorkspaceHandler) UpdateWorkspace(w http.ResponseWriter, r *http.Request) {
|
||||
// 检查权限(Admin)
|
||||
if !h.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
workspaceID := vars["workspace_id"]
|
||||
|
||||
workspace, err := h.workspaceService.GetByID(r.Context(), workspaceID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusNotFound, "Workspace not found", "")
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdateWorkspaceRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body", "")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
workspace.Name = req.Name
|
||||
}
|
||||
if req.Description != "" {
|
||||
workspace.Description = req.Description
|
||||
}
|
||||
|
||||
if err := h.workspaceService.Update(r.Context(), workspace); err != nil {
|
||||
respondError(w, http.StatusBadRequest, err.Error(), "")
|
||||
return
|
||||
}
|
||||
|
||||
respondSuccess(w, "", dto.WorkspaceDTOFromEntity(workspace))
|
||||
}
|
||||
|
||||
// DeleteWorkspace 删除工作空间
|
||||
// @Summary 删除工作空间
|
||||
// @Description 删除指定工作空间(Admin 专用)
|
||||
// @Tags workspace
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param workspace_id path string true "工作空间 ID"
|
||||
// @Success 200
|
||||
// @Router /workspaces/{workspace_id} [delete]
|
||||
func (h *WorkspaceHandler) DeleteWorkspace(w http.ResponseWriter, r *http.Request) {
|
||||
// 检查权限(Admin)
|
||||
if !h.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
workspaceID := vars["workspace_id"]
|
||||
|
||||
if err := h.workspaceService.Delete(r.Context(), workspaceID); err != nil {
|
||||
respondError(w, http.StatusBadRequest, err.Error(), "")
|
||||
return
|
||||
}
|
||||
|
||||
respondSuccess(w, "", nil)
|
||||
}
|
||||
|
||||
// ListWorkspaces 列出所有工作空间
|
||||
// @Summary 列出所有工作空间
|
||||
// @Description 获取所有工作空间列表(Admin 专用)
|
||||
// @Tags workspace
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} dto.WorkspaceListResponse
|
||||
// @Router /workspaces [get]
|
||||
func (h *WorkspaceHandler) ListWorkspaces(w http.ResponseWriter, r *http.Request) {
|
||||
// 检查权限(Admin)
|
||||
if !h.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
workspaces, err := h.workspaceService.List(r.Context())
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, err.Error(), "")
|
||||
return
|
||||
}
|
||||
|
||||
respondSuccess(w, "", dto.WorkspaceListResponse{
|
||||
Workspaces: dto.WorkspaceDTOsFromEntities(workspaces),
|
||||
Total: len(workspaces),
|
||||
})
|
||||
}
|
||||
|
||||
// GetWorkspaceQuotas 获取工作空间配额
|
||||
// @Summary 获取工作空间配额
|
||||
// @Description 获取指定工作空间的资源配额
|
||||
// @Tags workspace
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param workspace_id path string true "工作空间 ID"
|
||||
// @Success 200 {array} dto.QuotaDTO
|
||||
// @Router /workspaces/{workspace_id}/quotas [get]
|
||||
func (h *WorkspaceHandler) GetWorkspaceQuotas(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
workspaceID := vars["workspace_id"]
|
||||
|
||||
// 检查访问权限
|
||||
if !h.canAccessWorkspace(w, r, workspaceID) {
|
||||
return
|
||||
}
|
||||
|
||||
quotas, err := h.workspaceService.GetQuotas(r.Context(), workspaceID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, err.Error(), "")
|
||||
return
|
||||
}
|
||||
|
||||
respondSuccess(w, "", dto.QuotaDTOsFromEntities(quotas))
|
||||
}
|
||||
|
||||
// SetWorkspaceQuotas 设置工作空间配额
|
||||
// @Summary 设置工作空间配额
|
||||
// @Description 设置指定工作空间的 CPU/GPU/GPU Memory 配额(Admin 专用)
|
||||
// @Tags workspace
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param workspace_id path string true "工作空间 ID"
|
||||
// @Param request body dto.SetQuotasRequest true "配额设置请求"
|
||||
// @Success 200 {array} dto.QuotaDTO
|
||||
// @Router /workspaces/{workspace_id}/quotas [put]
|
||||
func (h *WorkspaceHandler) SetWorkspaceQuotas(w http.ResponseWriter, r *http.Request) {
|
||||
// 检查权限(Admin)
|
||||
if !h.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
workspaceID := vars["workspace_id"]
|
||||
|
||||
var req dto.SetQuotasRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body", "")
|
||||
return
|
||||
}
|
||||
|
||||
quotas := make(map[entity.ResourceType]struct {
|
||||
HardLimit float64
|
||||
SoftLimit float64
|
||||
})
|
||||
|
||||
if req.CPU != nil {
|
||||
quotas[entity.ResourceCPU] = struct {
|
||||
HardLimit float64
|
||||
SoftLimit float64
|
||||
}{req.CPU.HardLimit, req.CPU.SoftLimit}
|
||||
}
|
||||
if req.GPU != nil {
|
||||
quotas[entity.ResourceGPU] = struct {
|
||||
HardLimit float64
|
||||
SoftLimit float64
|
||||
}{req.GPU.HardLimit, req.GPU.SoftLimit}
|
||||
}
|
||||
if req.GPUMemory != nil {
|
||||
quotas[entity.ResourceGPUMemory] = struct {
|
||||
HardLimit float64
|
||||
SoftLimit float64
|
||||
}{req.GPUMemory.HardLimit, req.GPUMemory.SoftLimit}
|
||||
}
|
||||
|
||||
if err := h.workspaceService.SetQuotas(r.Context(), workspaceID, quotas); err != nil {
|
||||
respondError(w, http.StatusBadRequest, err.Error(), "")
|
||||
return
|
||||
}
|
||||
|
||||
// 返回更新后的配额
|
||||
updatedQuotas, _ := h.workspaceService.GetQuotas(r.Context(), workspaceID)
|
||||
respondSuccess(w, "", dto.QuotaDTOsFromEntities(updatedQuotas))
|
||||
}
|
||||
|
||||
// requireAdmin 检查是否为 Admin
|
||||
func (h *WorkspaceHandler) requireAdmin(w http.ResponseWriter, r *http.Request) bool {
|
||||
userRole := r.Header.Get("X-User-Role")
|
||||
if userRole != string(entity.RoleAdmin) {
|
||||
respondError(w, http.StatusForbidden, "Admin access required", "")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// canAccessWorkspace 检查是否可以访问工作空间
|
||||
func (h *WorkspaceHandler) canAccessWorkspace(w http.ResponseWriter, r *http.Request, workspaceID string) bool {
|
||||
userRole := r.Header.Get("X-User-Role")
|
||||
userWorkspaceID := r.Header.Get("X-Workspace-ID")
|
||||
|
||||
// Admin 可以访问所有
|
||||
if userRole == string(entity.RoleAdmin) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 普通用户只能访问自己的 workspace
|
||||
if userWorkspaceID != workspaceID {
|
||||
respondError(w, http.StatusForbidden, "Access denied", "")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user