feat: complete E2E deployment flow with storage layered config and values template versioning

- 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
This commit is contained in:
Ivan087
2026-04-30 16:31:00 +08:00
parent 985369d40f
commit 47849042a7
42 changed files with 2029 additions and 255 deletions

View File

@ -83,14 +83,14 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
// 获取用户信息
// TODO: 从 token 解析用户信息或从服务获取
// 返回响应
// 返回响应 - 使用 respondSuccess 包装,与其他 API 保持一致
response := &dto.AuthResponse{
AccessToken: accessToken,
RefreshToken: refreshToken,
Username: req.Username,
}
respondJSON(w, http.StatusOK, response)
respondSuccess(w, "Login successful", response)
}
// RefreshToken 刷新 Token
@ -117,11 +117,11 @@ func (h *AuthHandler) RefreshToken(w http.ResponseWriter, r *http.Request) {
return
}
// 返回响应
// 返回响应 - 使用 respondSuccess 包装
response := &dto.AuthResponse{
AccessToken: newAccessToken,
RefreshToken: req.RefreshToken,
}
respondJSON(w, http.StatusOK, response)
respondSuccess(w, "Token refreshed", response)
}

View File

@ -188,6 +188,7 @@ func (h *InstanceHandler) ListInstances(w http.ResponseWriter, r *http.Request)
LastOperation: string(instance.LastOperation),
LastError: instance.LastError,
Revision: instance.Revision,
Values: instance.Values,
CreatedAt: instance.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: instance.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
})

View File

@ -10,6 +10,14 @@ import (
"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
@ -77,6 +85,7 @@ func (h *StorageHandler) CreateStorage(w http.ResponseWriter, r *http.Request) {
req.Description,
req.IsDefault,
req.IsShared,
req.ClusterID,
)
if err != nil {
respondError(w, http.StatusBadRequest, "Failed to create storage backend", err.Error())
@ -252,6 +261,45 @@ func (h *StorageHandler) DeleteStorage(w http.ResponseWriter, r *http.Request) {
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{}
@ -278,6 +326,7 @@ func toStorageResponse(storage *entity.StorageBackend) *dto.StorageResponse {
return &dto.StorageResponse{
ID: storage.ID,
WorkspaceID: storage.WorkspaceID,
ClusterID: storage.ClusterID,
OwnerID: storage.OwnerID,
Name: storage.Name,
Type: string(storage.Type),

View File

@ -26,7 +26,7 @@ func NewWorkspaceHandler(workspaceService *service.WorkspaceService, authService
// CreateWorkspace 创建工作空间
// @Summary 创建工作空间
// @Description 创建新的工作空间Admin 专用)
// @Description 创建新的工作空间Admin 专用,支持 cluster_ids 和初始配额
// @Tags workspace
// @Accept json
// @Produce json
@ -48,7 +48,31 @@ func (h *WorkspaceHandler) CreateWorkspace(w http.ResponseWriter, r *http.Reques
// 获取创建者 ID
userID := GetUserIDFromRequest(r)
workspace, err := h.workspaceService.Create(r.Context(), req.Name, req.Description, userID)
// 准备配额
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}
}
workspace, err := h.workspaceService.Create(r.Context(), req.Name, req.Description, userID, req.ClusterIDs, quotas)
if err != nil {
respondError(w, http.StatusBadRequest, err.Error(), "")
return
@ -129,6 +153,9 @@ func (h *WorkspaceHandler) UpdateWorkspace(w http.ResponseWriter, r *http.Reques
if req.Description != "" {
workspace.Description = req.Description
}
if req.ClusterIDs != nil {
workspace.ClusterIDs = req.ClusterIDs
}
if err := h.workspaceService.Update(r.Context(), workspace); err != nil {
respondError(w, http.StatusBadRequest, err.Error(), "")