package service import ( "context" "sort" "time" "github.com/google/uuid" "github.com/ocdp/cluster-service/internal/domain/entity" "github.com/ocdp/cluster-service/internal/domain/repository" "github.com/ocdp/cluster-service/internal/pkg/authz" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" ) type WorkspaceService struct { workspaceRepo repository.WorkspaceRepository bindingRepo repository.WorkspaceClusterBindingRepository clusterRepo repository.ClusterRepository tenantClient repository.TenantKubeClient auditRepo repository.AuditLogRepository } func NewWorkspaceService( workspaceRepo repository.WorkspaceRepository, bindingRepo repository.WorkspaceClusterBindingRepository, clusterRepo repository.ClusterRepository, tenantClient repository.TenantKubeClient, auditRepo repository.AuditLogRepository, ) *WorkspaceService { return &WorkspaceService{ workspaceRepo: workspaceRepo, bindingRepo: bindingRepo, clusterRepo: clusterRepo, tenantClient: tenantClient, auditRepo: auditRepo, } } func (s *WorkspaceService) ListWorkspaces(ctx context.Context) ([]*entity.Workspace, error) { principal, err := authz.RequirePrincipal(ctx) if err != nil { return nil, entity.ErrUnauthorized } if principal.IsAdmin() { return s.workspaceRepo.List(ctx) } workspace, err := s.workspaceRepo.GetByID(ctx, principal.WorkspaceID) if err != nil { return nil, err } return []*entity.Workspace{workspace}, nil } func (s *WorkspaceService) CreateWorkspace(ctx context.Context, name string) (*entity.Workspace, error) { principal, err := authz.RequirePrincipal(ctx) if err != nil { return nil, entity.ErrUnauthorized } if !principal.IsAdmin() { return nil, entity.ErrForbidden } workspace := entity.NewWorkspace(name, principal.UserID) workspace.ID = uuid.New().String() if err := s.workspaceRepo.Create(ctx, workspace); err != nil { return nil, err } s.audit(ctx, principal, "create", "workspace", workspace.ID, workspace.Name, nil) return workspace, nil } func (s *WorkspaceService) EnsureClusterBinding(ctx context.Context, workspaceID, clusterID string) (*entity.WorkspaceClusterBinding, error) { principal, err := authz.RequirePrincipal(ctx) if err != nil { return nil, entity.ErrUnauthorized } if !principal.IsAdmin() && workspaceID != principal.WorkspaceID { return nil, entity.ErrForbidden } workspace, err := s.workspaceRepo.GetByID(ctx, workspaceID) if err != nil { return nil, err } cluster, err := s.clusterRepo.GetByID(ctx, clusterID) if err != nil { return nil, entity.ErrClusterNotFound } if !principal.IsAdmin() && !authz.CanReadResource(principal, cluster.WorkspaceID, cluster.OwnerID, cluster.Visibility) { return nil, entity.ErrClusterNotFound } binding := &entity.WorkspaceClusterBinding{ ID: uuid.New().String(), WorkspaceID: workspace.ID, ClusterID: cluster.ID, Namespace: workspace.K8sNamespace, ServiceAccount: workspace.K8sSAName, QuotaCPU: workspace.QuotaCPU, QuotaMemory: workspace.QuotaMemory, QuotaGPU: workspace.QuotaGPU, QuotaGPUMem: workspace.QuotaGPUMem, Status: "active", CreatedAt: time.Now(), UpdatedAt: time.Now(), } tenantBinding := entity.NewTenantBinding(binding.Namespace) tenantBinding.ServiceAccountName = binding.ServiceAccount tenantBinding.ResourceQuotaHard = resourceQuotaHard(workspace) if s.tenantClient != nil { if err := s.tenantClient.EnsureTenant(ctx, cluster, tenantBinding); err != nil { return nil, err } } if err := s.bindingRepo.Upsert(ctx, binding); err != nil { return nil, err } s.audit(ctx, principal, "init", "workspace_cluster_binding", binding.ID, binding.Namespace, map[string]interface{}{"cluster_id": clusterID}) return binding, nil } func (s *WorkspaceService) IssueKubeconfig(ctx context.Context, workspaceID, clusterID string, ttl time.Duration) (*entity.TenantKubeconfig, error) { principal, err := authz.RequirePrincipal(ctx) if err != nil { return nil, entity.ErrUnauthorized } if !principal.IsAdmin() && workspaceID != principal.WorkspaceID { return nil, entity.ErrForbidden } workspace, err := s.workspaceRepo.GetByID(ctx, workspaceID) if err != nil { return nil, err } if workspace.Status == entity.WorkspaceSuspended { return nil, entity.ErrWorkspaceSuspended } cluster, err := s.clusterRepo.GetByID(ctx, clusterID) if err != nil { return nil, entity.ErrClusterNotFound } if !principal.IsAdmin() && !authz.CanReadResource(principal, cluster.WorkspaceID, cluster.OwnerID, cluster.Visibility) { return nil, entity.ErrClusterNotFound } binding, err := s.bindingRepo.Get(ctx, workspaceID, clusterID) if err != nil { binding, err = s.EnsureClusterBinding(ctx, workspaceID, clusterID) if err != nil { return nil, err } } tenantBinding := entity.NewTenantBinding(binding.Namespace) tenantBinding.ServiceAccountName = binding.ServiceAccount tenantBinding.ResourceQuotaHard = resourceQuotaHard(workspace) kubeconfig, err := s.tenantClient.IssueKubeconfig(ctx, cluster, tenantBinding, ttl) if err != nil { return nil, err } s.audit(ctx, principal, "issue_kubeconfig", "workspace_cluster_binding", binding.ID, binding.Namespace, map[string]interface{}{"cluster_id": clusterID, "ttl_seconds": int64(entity.TenantTokenTTL(ttl).Seconds())}) return kubeconfig, nil } func resourceQuotaHard(workspace *entity.Workspace) corev1.ResourceList { hard := corev1.ResourceList{} addQuantity := func(name corev1.ResourceName, value string) { value = normalizeStandardQuotaQuantity(value) if value == "" { return } if quantity, err := resource.ParseQuantity(value); err == nil { hard[name] = quantity } } addGPUMemoryQuantity := func(value string) { value, err := normalizeGPUMemoryQuota(value) if err != nil || value == "" { return } if quantity, err := resource.ParseQuantity(value); err == nil { hard[corev1.ResourceName("requests.nvidia.com/gpumem")] = quantity } } if workspace == nil { return hard } addQuantity(corev1.ResourceName("requests.cpu"), workspace.QuotaCPU) addQuantity(corev1.ResourceName("requests.memory"), workspace.QuotaMemory) addQuantity(corev1.ResourceName("requests.nvidia.com/gpu"), workspace.QuotaGPU) addGPUMemoryQuantity(workspace.QuotaGPUMem) return hard } func (s *WorkspaceService) IssueCurrentKubeconfig(ctx context.Context, requestedClusterID string, ttl time.Duration) (*entity.TenantKubeconfig, error) { principal, err := authz.RequirePrincipal(ctx) if err != nil { return nil, entity.ErrUnauthorized } if requestedClusterID != "" { return s.IssueKubeconfig(ctx, principal.WorkspaceID, requestedClusterID, ttl) } workspace, err := s.workspaceRepo.GetByID(ctx, principal.WorkspaceID) if err != nil { return nil, err } if workspace.DefaultClusterID != "" { return s.IssueKubeconfig(ctx, principal.WorkspaceID, workspace.DefaultClusterID, ttl) } return s.IssueDefaultKubeconfig(ctx, ttl) } func (s *WorkspaceService) IssueDefaultKubeconfig(ctx context.Context, ttl time.Duration) (*entity.TenantKubeconfig, error) { principal, err := authz.RequirePrincipal(ctx) if err != nil { return nil, entity.ErrUnauthorized } clusters, err := s.clusterRepo.List(ctx) if err != nil { return nil, err } candidates := make([]*entity.Cluster, 0, len(clusters)) for _, cluster := range clusters { if !authz.CanReadResource(principal, cluster.WorkspaceID, cluster.OwnerID, cluster.Visibility) { continue } switch cluster.Visibility { case authz.VisibilityGlobalShared: candidates = append(candidates, cluster) case authz.VisibilityWorkspaceShared: if cluster.WorkspaceID == principal.WorkspaceID { candidates = append(candidates, cluster) } } } sort.SliceStable(candidates, func(i, j int) bool { leftRank := defaultKubeconfigClusterRank(candidates[i]) rightRank := defaultKubeconfigClusterRank(candidates[j]) if leftRank != rightRank { return leftRank < rightRank } return candidates[i].Name < candidates[j].Name }) var firstIssueErr error for _, cluster := range candidates { if kubeconfig, err := s.IssueKubeconfig(ctx, principal.WorkspaceID, cluster.ID, ttl); err == nil { return kubeconfig, nil } else if firstIssueErr == nil { firstIssueErr = err } } if firstIssueErr != nil { return nil, firstIssueErr } return nil, entity.ErrClusterNotFound } func defaultKubeconfigClusterRank(cluster *entity.Cluster) int { switch cluster.Visibility { case authz.VisibilityGlobalShared: return 0 case authz.VisibilityWorkspaceShared: return 1 default: return 2 } } func (s *WorkspaceService) SuspendWorkspace(ctx context.Context, workspaceID string) error { principal, err := authz.RequirePrincipal(ctx) if err != nil { return entity.ErrUnauthorized } if !principal.IsAdmin() { return entity.ErrForbidden } workspace, err := s.workspaceRepo.GetByID(ctx, workspaceID) if err != nil { return err } workspace.Status = entity.WorkspaceSuspended if err := s.workspaceRepo.Update(ctx, workspace); err != nil { return err } clusters, _ := s.clusterRepo.List(ctx) for _, cluster := range clusters { binding, err := s.bindingRepo.Get(ctx, workspaceID, cluster.ID) if err != nil { continue } tenantBinding := entity.NewTenantBinding(binding.Namespace) tenantBinding.ServiceAccountName = binding.ServiceAccount _ = s.tenantClient.SuspendTenant(ctx, cluster, tenantBinding) } s.audit(ctx, principal, "suspend", "workspace", workspace.ID, workspace.Name, nil) return nil } func (s *WorkspaceService) audit(ctx context.Context, principal *authz.Principal, action, resourceType, resourceID, resourceName string, details map[string]interface{}) { if s.auditRepo == nil || principal == nil { return } _ = s.auditRepo.Create(ctx, &entity.AuditLog{ WorkspaceID: principal.WorkspaceID, UserID: principal.UserID, Action: action, ResourceType: resourceType, ResourceID: resourceID, ResourceName: resourceName, Details: details, CreatedAt: time.Now(), }) }