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:
291
backend/internal/adapter/input/http/rest/storage_handler.go
Normal file
291
backend/internal/adapter/input/http/rest/storage_handler.go
Normal file
@ -0,0 +1,291 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/ocdp/cluster-service/internal/adapter/input/http/dto"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/service"
|
||||
)
|
||||
|
||||
// StorageHandler Storage Backend Handler
|
||||
type StorageHandler struct {
|
||||
storageService *service.StorageService
|
||||
}
|
||||
|
||||
// NewStorageHandler 创建 Storage Handler
|
||||
func NewStorageHandler(storageService *service.StorageService) *StorageHandler {
|
||||
return &StorageHandler{
|
||||
storageService: storageService,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateStorage 创建存储后端
|
||||
// @Summary 创建存储后端
|
||||
// @Description 新增存储后端配置(NFS/PV/hostPath)
|
||||
// @Tags Storage
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body dto.CreateStorageRequest true "存储后端信息"
|
||||
// @Success 201 {object} dto.StorageResponse
|
||||
// @Failure 400 {object} dto.ErrorResponse
|
||||
// @Router /storage-backends [post]
|
||||
func (h *StorageHandler) CreateStorage(w http.ResponseWriter, r *http.Request) {
|
||||
var req dto.CreateStorageRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
workspaceID := r.Header.Get("X-Workspace-ID")
|
||||
ownerID := r.Header.Get("X-User-ID")
|
||||
|
||||
// 构建配置
|
||||
storageType := entity.StorageType(req.Type)
|
||||
config := entity.StorageConfig{}
|
||||
|
||||
switch storageType {
|
||||
case entity.StorageTypeNFS:
|
||||
config.NFS = &entity.NFSConfig{
|
||||
Server: req.NFS.Server,
|
||||
Path: req.NFS.Path,
|
||||
}
|
||||
case entity.StorageTypePV:
|
||||
config.PV = &entity.PVConfig{
|
||||
StorageClassName: req.PV.StorageClassName,
|
||||
Capacity: req.PV.Capacity,
|
||||
AccessModes: req.PV.AccessModes,
|
||||
}
|
||||
case entity.StorageTypeHostPath:
|
||||
config.HostPath = &entity.HostPathConfig{
|
||||
Path: req.HostPath.Path,
|
||||
}
|
||||
}
|
||||
|
||||
// 调用领域服务
|
||||
storage, err := h.storageService.Create(
|
||||
r.Context(),
|
||||
workspaceID,
|
||||
ownerID,
|
||||
req.Name,
|
||||
storageType,
|
||||
config,
|
||||
req.Description,
|
||||
req.IsDefault,
|
||||
req.IsShared,
|
||||
)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Failed to create storage backend", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
response := toStorageResponse(storage)
|
||||
respondJSON(w, http.StatusCreated, response)
|
||||
}
|
||||
|
||||
// GetStorage 获取存储后端详情
|
||||
// @Summary 获取存储后端
|
||||
// @Tags Storage
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param storage_id path string true "Storage ID"
|
||||
// @Success 200 {object} dto.StorageResponse
|
||||
// @Failure 404 {object} dto.ErrorResponse
|
||||
// @Router /storage-backends/{storage_id} [get]
|
||||
func (h *StorageHandler) GetStorage(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
storageID := vars["storage_id"]
|
||||
|
||||
storage, err := h.storageService.GetByID(r.Context(), storageID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusNotFound, "Storage backend not found", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response := toStorageResponse(storage)
|
||||
respondJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetAllStorage 获取所有存储后端
|
||||
// @Summary 列出所有存储后端
|
||||
// @Tags Storage
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {array} dto.StorageResponse
|
||||
// @Failure 500 {object} dto.ErrorResponse
|
||||
// @Router /storage-backends [get]
|
||||
func (h *StorageHandler) GetAllStorage(w http.ResponseWriter, r *http.Request) {
|
||||
// 获取 workspace_id(从 JWT)
|
||||
workspaceID := r.Header.Get("X-Workspace-ID")
|
||||
role := r.Header.Get("X-User-Role")
|
||||
|
||||
var storages []*entity.StorageBackend
|
||||
var err error
|
||||
|
||||
// Admin 可以看到所有,其他用户只看自己 workspace + 共享的
|
||||
if role == "admin" {
|
||||
storages, err = h.storageService.List(r.Context())
|
||||
} else if workspaceID != "" {
|
||||
// 获取 workspace 的存储 + 共享存储
|
||||
workspaceStorages, _ := h.storageService.GetByWorkspace(r.Context(), workspaceID)
|
||||
sharedStorages, _ := h.storageService.GetShared(r.Context())
|
||||
|
||||
// 合并去重
|
||||
seen := make(map[string]bool)
|
||||
for _, s := range workspaceStorages {
|
||||
if !seen[s.ID] {
|
||||
storages = append(storages, s)
|
||||
seen[s.ID] = true
|
||||
}
|
||||
}
|
||||
for _, s := range sharedStorages {
|
||||
if !seen[s.ID] {
|
||||
storages = append(storages, s)
|
||||
seen[s.ID] = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 没有 workspace 的用户只能看到共享存储
|
||||
storages, err = h.storageService.GetShared(r.Context())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "Failed to list storage backends", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
responses := make([]*dto.StorageResponse, 0, len(storages))
|
||||
for _, storage := range storages {
|
||||
responses = append(responses, toStorageResponse(storage))
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, responses)
|
||||
}
|
||||
|
||||
// UpdateStorage 更新存储后端
|
||||
// @Summary 更新存储后端
|
||||
// @Tags Storage
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param storage_id path string true "Storage ID"
|
||||
// @Param request body dto.UpdateStorageRequest true "更新内容"
|
||||
// @Success 200 {object} dto.StorageResponse
|
||||
// @Failure 404 {object} dto.ErrorResponse
|
||||
// @Router /storage-backends/{storage_id} [put]
|
||||
func (h *StorageHandler) UpdateStorage(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
storageID := vars["storage_id"]
|
||||
|
||||
var req dto.UpdateStorageRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 构建配置
|
||||
var storageType entity.StorageType
|
||||
if req.Type != "" {
|
||||
storageType = entity.StorageType(req.Type)
|
||||
}
|
||||
|
||||
config := entity.StorageConfig{}
|
||||
if storageType == entity.StorageTypeNFS && (req.NFS.Server != "" || req.NFS.Path != "") {
|
||||
config.NFS = &entity.NFSConfig{
|
||||
Server: req.NFS.Server,
|
||||
Path: req.NFS.Path,
|
||||
}
|
||||
} else if storageType == entity.StorageTypePV && req.PV.StorageClassName != "" {
|
||||
config.PV = &entity.PVConfig{
|
||||
StorageClassName: req.PV.StorageClassName,
|
||||
Capacity: req.PV.Capacity,
|
||||
AccessModes: req.PV.AccessModes,
|
||||
}
|
||||
} else if storageType == entity.StorageTypeHostPath && req.HostPath.Path != "" {
|
||||
config.HostPath = &entity.HostPathConfig{
|
||||
Path: req.HostPath.Path,
|
||||
}
|
||||
}
|
||||
|
||||
storage, err := h.storageService.Update(
|
||||
r.Context(),
|
||||
storageID,
|
||||
req.Name,
|
||||
req.Description,
|
||||
storageType,
|
||||
config,
|
||||
req.IsDefault,
|
||||
req.IsShared,
|
||||
)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Failed to update storage backend", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response := toStorageResponse(storage)
|
||||
respondJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
// DeleteStorage 删除存储后端
|
||||
// @Summary 删除存储后端
|
||||
// @Tags Storage
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param storage_id path string true "Storage ID"
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @Failure 404 {object} dto.ErrorResponse
|
||||
// @Router /storage-backends/{storage_id} [delete]
|
||||
func (h *StorageHandler) DeleteStorage(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
storageID := vars["storage_id"]
|
||||
|
||||
if err := h.storageService.Delete(r.Context(), storageID); err != nil {
|
||||
respondError(w, http.StatusNotFound, "Failed to delete storage backend", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// toStorageResponse 转换为响应 DTO
|
||||
func toStorageResponse(storage *entity.StorageBackend) *dto.StorageResponse {
|
||||
config := dto.StorageConfigDTO{}
|
||||
|
||||
if storage.Config.NFS != nil {
|
||||
config.NFS = &dto.NFSConfigDTO{
|
||||
Server: storage.Config.NFS.Server,
|
||||
Path: storage.Config.NFS.Path,
|
||||
}
|
||||
}
|
||||
if storage.Config.PV != nil {
|
||||
config.PV = &dto.PVConfigDTO{
|
||||
StorageClassName: storage.Config.PV.StorageClassName,
|
||||
Capacity: storage.Config.PV.Capacity,
|
||||
AccessModes: storage.Config.PV.AccessModes,
|
||||
}
|
||||
}
|
||||
if storage.Config.HostPath != nil {
|
||||
config.HostPath = &dto.HostPathConfigDTO{
|
||||
Path: storage.Config.HostPath.Path,
|
||||
}
|
||||
}
|
||||
|
||||
return &dto.StorageResponse{
|
||||
ID: storage.ID,
|
||||
WorkspaceID: storage.WorkspaceID,
|
||||
OwnerID: storage.OwnerID,
|
||||
Name: storage.Name,
|
||||
Type: string(storage.Type),
|
||||
Config: config,
|
||||
Description: storage.Description,
|
||||
IsDefault: storage.IsDefault,
|
||||
IsShared: storage.IsShared,
|
||||
CreatedAt: storage.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
UpdatedAt: storage.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user