- Instance deployment: charts browser, deploy modal, instances list - Values Template version management (create/history/rollback) - Storage layered config (cluster > workspace > shared priority) - Cluster credential decryptIfNeeded for mixed encrypted/plaintext kubeconfig - YAML syntax validation (client-side + server-side warning) - Frontend: charts, instances, storage, templates, admin pages - Backend: storage service, instance service, cluster service, helm client - Multi-Tenant Kubeconfig.md: added by user
340 lines
9.8 KiB
Go
340 lines
9.8 KiB
Go
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"
|
||
)
|
||
|
||
// StorageResolutionResponse 分层存储解析响应
|
||
type StorageResolutionResponse struct {
|
||
Storage *dto.StorageResponse `json:"storage,omitempty"`
|
||
ValuesYAML string `json:"values_yaml,omitempty"`
|
||
Source string `json:"source,omitempty"` // workspace, cluster, shared
|
||
Message string `json:"message,omitempty"`
|
||
}
|
||
|
||
// 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,
|
||
req.ClusterID,
|
||
)
|
||
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)
|
||
}
|
||
|
||
// ResolveStorage 预览分层存储解析结果
|
||
// @Summary 预览分层存储解析结果
|
||
// @Description 根据 cluster_id 和 workspace_id 解析出最终生效的存储配置
|
||
// @Tags Storage
|
||
// @Produce json
|
||
// @Security BearerAuth
|
||
// @Param cluster_id query string false "Cluster ID"
|
||
// @Param workspace_id query string false "Workspace ID"
|
||
// @Success 200 {object} StorageResolutionResponse
|
||
// @Router /storage-backends/resolve [get]
|
||
func (h *StorageHandler) ResolveStorage(w http.ResponseWriter, r *http.Request) {
|
||
clusterID := r.URL.Query().Get("cluster_id")
|
||
workspaceID := r.URL.Query().Get("workspace_id")
|
||
if workspaceID == "" {
|
||
workspaceID = r.Header.Get("X-Workspace-ID")
|
||
}
|
||
|
||
resolution, err := h.storageService.ResolveStorageConfig(r.Context(), clusterID, workspaceID)
|
||
if err != nil {
|
||
respondError(w, http.StatusInternalServerError, "Failed to resolve storage config", err.Error())
|
||
return
|
||
}
|
||
|
||
if resolution == nil || resolution.Storage == nil {
|
||
respondJSON(w, http.StatusOK, &StorageResolutionResponse{
|
||
Message: "No default storage configured",
|
||
})
|
||
return
|
||
}
|
||
|
||
response := &StorageResolutionResponse{
|
||
Storage: toStorageResponse(resolution.Storage),
|
||
ValuesYAML: resolution.ValuesYAML,
|
||
Source: resolution.Source,
|
||
}
|
||
|
||
respondJSON(w, http.StatusOK, response)
|
||
}
|
||
|
||
// 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,
|
||
ClusterID: storage.ClusterID,
|
||
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"),
|
||
}
|
||
} |