- Add Workspace domain (entity, repository, service, handler, DTO) - Add multi-tenant K8s client with tenant binding and quota management - Add K8s diagnostics client (instance diagnostics) - Add authorization middleware (authz package) - Restructure frontend to feature-based architecture (features/) - Add User Management page in configuration - Add AccessDenied page and route guards - Refactor shared components (form inputs, layout, UI) - Update Tailwind config for new design system - Add comprehensive documentation (docs/, tasks/, plans) - Improve cluster service with better kubeconfig handling - Add tests for crypto, config, helm client, tenant binding
166 lines
5.8 KiB
Go
166 lines
5.8 KiB
Go
package rest
|
|
|
|
import (
|
|
"encoding/json"
|
|
"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")
|
|
}
|
|
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) {
|
|
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.ErrClusterNotFound, entity.ErrRegistryNotFound, entity.ErrInstanceNotFound, entity.ErrWorkspaceNotFound:
|
|
respondError(w, http.StatusNotFound, fallback, err.Error())
|
|
default:
|
|
respondError(w, http.StatusBadRequest, fallback, err.Error())
|
|
}
|
|
}
|