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:
@ -76,6 +76,7 @@ func WorkspaceDTOFromEntity(workspace *entity.Workspace) *WorkspaceDTO {
|
||||
return &WorkspaceDTO{
|
||||
ID: workspace.ID,
|
||||
Name: workspace.Name,
|
||||
ClusterIDs: workspace.ClusterIDs,
|
||||
Description: workspace.Description,
|
||||
CreatedBy: workspace.CreatedBy,
|
||||
CreatedAt: workspace.CreatedAt,
|
||||
|
||||
@ -7,6 +7,7 @@ type CreateStorageRequest struct {
|
||||
Description string `json:"description"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
IsShared bool `json:"is_shared"`
|
||||
ClusterID string `json:"cluster_id,omitempty"` // 用于 cluster-level storage
|
||||
|
||||
// NFS 配置
|
||||
NFS NFSConfigDTO `json:"nfs,omitempty"`
|
||||
@ -23,6 +24,7 @@ type UpdateStorageRequest struct {
|
||||
Description string `json:"description"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
IsShared bool `json:"is_shared"`
|
||||
ClusterID string `json:"cluster_id,omitempty"` // 用于 cluster-level storage
|
||||
|
||||
// NFS 配置
|
||||
NFS NFSConfigDTO `json:"nfs,omitempty"`
|
||||
@ -52,17 +54,18 @@ type HostPathConfigDTO struct {
|
||||
|
||||
// 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"`
|
||||
ID string `json:"id"`
|
||||
WorkspaceID string `json:"workspace_id,omitempty"`
|
||||
ClusterID string `json:"cluster_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 存储配置(脱敏后)
|
||||
|
||||
@ -4,24 +4,32 @@ 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"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ClusterIDs []string `json:"cluster_ids,omitempty"`
|
||||
Quotas []*QuotaDTO `json:"quotas,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
CreatedBy string `json:"created_by"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// CreateWorkspaceRequest 创建工作空间请求
|
||||
// CreateWorkspaceRequest 创建工作空间请求(包含配额设置)
|
||||
type CreateWorkspaceRequest struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Description string `json:"description"`
|
||||
ClusterIDs []string `json:"cluster_ids"`
|
||||
// Quotas can be set during creation
|
||||
CPU *QuotaValue `json:"cpu"`
|
||||
GPU *QuotaValue `json:"gpu"`
|
||||
GPUMemory *QuotaValue `json:"gpu_memory"`
|
||||
}
|
||||
|
||||
// UpdateWorkspaceRequest 更新工作空间请求
|
||||
type UpdateWorkspaceRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
ClusterIDs []string `json:"cluster_ids"`
|
||||
}
|
||||
|
||||
// QuotaDTO 配额 DTO
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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"),
|
||||
})
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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(), "")
|
||||
|
||||
Reference in New Issue
Block a user