package rest import ( "encoding/json" "errors" "net/http" "time" "github.com/gorilla/mux" "github.com/ocdp/cluster-service/internal/domain/entity" "github.com/ocdp/cluster-service/internal/domain/service" "github.com/ocdp/cluster-service/internal/pkg/authz" ) type WorkspaceHandler struct { workspaceService *service.WorkspaceService } func NewWorkspaceHandler(workspaceService *service.WorkspaceService) *WorkspaceHandler { return &WorkspaceHandler{workspaceService: workspaceService} } type createWorkspaceRequest struct { Name string `json:"name"` } type workspaceResponse struct { ID string `json:"id"` Name string `json:"name"` Status string `json:"status"` K8sNamespace string `json:"k8sNamespace"` K8sSAName string `json:"k8sSaName"` DefaultClusterID string `json:"defaultClusterId,omitempty"` QuotaCPU string `json:"quotaCpu,omitempty"` QuotaMemory string `json:"quotaMemory,omitempty"` QuotaGPU string `json:"quotaGpu,omitempty"` QuotaGPUMem string `json:"quotaGpuMemory,omitempty"` CreatedBy string `json:"createdBy"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` } type bindClusterRequest struct { ClusterID string `json:"clusterId"` } type kubeconfigRequest struct { ClusterID string `json:"clusterId"` TTLSeconds int64 `json:"ttlSeconds"` } func (h *WorkspaceHandler) ListWorkspaces(w http.ResponseWriter, r *http.Request) { workspaces, err := h.workspaceService.ListWorkspaces(r.Context()) if err != nil { respondServiceError(w, err, "Failed to list workspaces") return } response := make([]workspaceResponse, 0, len(workspaces)) for _, workspace := range workspaces { response = append(response, toWorkspaceResponse(workspace)) } respondJSON(w, http.StatusOK, response) } func (h *WorkspaceHandler) CreateWorkspace(w http.ResponseWriter, r *http.Request) { var req createWorkspaceRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body", err.Error()) return } workspace, err := h.workspaceService.CreateWorkspace(r.Context(), req.Name) if err != nil { respondServiceError(w, err, "Failed to create workspace") return } respondJSON(w, http.StatusCreated, toWorkspaceResponse(workspace)) } func (h *WorkspaceHandler) InitClusterBinding(w http.ResponseWriter, r *http.Request) { workspaceID := mux.Vars(r)["workspace_id"] var req bindClusterRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body", err.Error()) return } binding, err := h.workspaceService.EnsureClusterBinding(r.Context(), workspaceID, req.ClusterID) if err != nil { respondServiceError(w, err, "Failed to initialize workspace cluster binding") return } respondJSON(w, http.StatusOK, binding) } func (h *WorkspaceHandler) IssueKubeconfig(w http.ResponseWriter, r *http.Request) { workspaceID := mux.Vars(r)["workspace_id"] var req kubeconfigRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body", err.Error()) return } kubeconfig, err := h.workspaceService.IssueKubeconfig(r.Context(), workspaceID, req.ClusterID, time.Duration(req.TTLSeconds)*time.Second) if err != nil { respondServiceError(w, err, "Failed to issue kubeconfig") return } respondJSON(w, http.StatusOK, map[string]interface{}{ "kubeconfig": kubeconfig.Kubeconfig, "expiresAt": kubeconfig.ExpiresAt.Format(time.RFC3339), }) } func (h *WorkspaceHandler) IssueCurrentKubeconfig(w http.ResponseWriter, r *http.Request) { clusterID := r.URL.Query().Get("clusterId") if clusterID == "" { clusterID = r.URL.Query().Get("cluster_id") } h.issueCurrentKubeconfigForCluster(w, r, clusterID) } func (h *WorkspaceHandler) IssueClusterKubeconfig(w http.ResponseWriter, r *http.Request) { clusterID := mux.Vars(r)["cluster_id"] h.issueCurrentKubeconfigForCluster(w, r, clusterID) } func (h *WorkspaceHandler) issueCurrentKubeconfigForCluster(w http.ResponseWriter, r *http.Request, clusterID string) { kubeconfig, err := h.workspaceService.IssueCurrentKubeconfig(r.Context(), clusterID, 2*time.Hour) if err != nil { respondServiceError(w, err, "Failed to issue kubeconfig") return } w.Header().Set("Content-Type", "application/x-yaml") w.Header().Set("X-OCDP-Kubeconfig-Expires-At", kubeconfig.ExpiresAt.Format(time.RFC3339)) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(kubeconfig.Kubeconfig)) } func (h *WorkspaceHandler) SuspendWorkspace(w http.ResponseWriter, r *http.Request) { workspaceID := mux.Vars(r)["workspace_id"] if err := h.workspaceService.SuspendWorkspace(r.Context(), workspaceID); err != nil { respondServiceError(w, err, "Failed to suspend workspace") return } w.WriteHeader(http.StatusNoContent) } func toWorkspaceResponse(workspace *entity.Workspace) workspaceResponse { return workspaceResponse{ ID: workspace.ID, Name: workspace.Name, Status: string(workspace.Status), K8sNamespace: workspace.K8sNamespace, K8sSAName: workspace.K8sSAName, DefaultClusterID: workspace.DefaultClusterID, QuotaCPU: workspace.QuotaCPU, QuotaMemory: workspace.QuotaMemory, QuotaGPU: workspace.QuotaGPU, QuotaGPUMem: workspace.QuotaGPUMem, CreatedBy: workspace.CreatedBy, CreatedAt: workspace.CreatedAt.Format(time.RFC3339), UpdatedAt: workspace.UpdatedAt.Format(time.RFC3339), } } func respondServiceError(w http.ResponseWriter, err error, fallback string) { if errors.Is(err, service.ErrQuotaExceeded) { respondError(w, http.StatusUnprocessableEntity, "Quota exceeded", err.Error()) return } switch err { case entity.ErrUnauthorized, authz.ErrUnauthenticated: respondError(w, http.StatusUnauthorized, "Unauthorized", err.Error()) case entity.ErrForbidden, authz.ErrForbidden, entity.ErrUserInactive, entity.ErrWorkspaceSuspended: respondError(w, http.StatusForbidden, "Forbidden", err.Error()) case entity.ErrWorkspaceNamespaceConflict, entity.ErrUserHasInstances, entity.ErrWorkspaceExists, entity.ErrInstanceExists: respondError(w, http.StatusConflict, "Conflict", err.Error()) case entity.ErrProtectedNamespace: respondError(w, http.StatusForbidden, "Forbidden", err.Error()) case entity.ErrClusterNotFound, entity.ErrRegistryNotFound, entity.ErrInstanceNotFound, entity.ErrWorkspaceNotFound: respondError(w, http.StatusNotFound, fallback, err.Error()) default: respondError(w, http.StatusBadRequest, fallback, err.Error()) } }