refactor: full-stack restructure with multi-tenancy, workspace management, and K8s diagnostics
- 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
This commit is contained in:
123
backend/internal/domain/entity/tenant_binding.go
Normal file
123
backend/internal/domain/entity/tenant_binding.go
Normal file
@ -0,0 +1,123 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultTenantServiceAccountName = "tenant-admin"
|
||||
DefaultTenantRoleBindingName = "tenant-admin"
|
||||
DefaultTenantClusterRoleName = "admin"
|
||||
DefaultTenantResourceQuotaName = "tenant-quota"
|
||||
MaxTenantKubeconfigTTL = 2 * time.Hour
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidTenantNamespace = errors.New("invalid tenant namespace")
|
||||
ErrInvalidTenantServiceAccount = errors.New("invalid tenant service account")
|
||||
ErrInvalidTenantRoleBinding = errors.New("invalid tenant role binding")
|
||||
ErrInvalidTenantClusterRole = errors.New("invalid tenant cluster role")
|
||||
ErrInvalidTenantResourceQuota = errors.New("invalid tenant resource quota")
|
||||
ErrInvalidTenantKubeconfigToken = errors.New("invalid tenant kubeconfig token")
|
||||
)
|
||||
|
||||
// TenantBinding describes the Kubernetes resources that grant a workspace access
|
||||
// to one tenant namespace. It intentionally excludes credential material.
|
||||
type TenantBinding struct {
|
||||
Namespace string
|
||||
ServiceAccountName string
|
||||
RoleBindingName string
|
||||
ClusterRoleName string
|
||||
ResourceQuotaName string
|
||||
Labels map[string]string
|
||||
Annotations map[string]string
|
||||
ResourceQuotaHard corev1.ResourceList
|
||||
}
|
||||
|
||||
// TenantKubeconfig contains a short-lived kubeconfig and its expiration time.
|
||||
// Callers must treat Kubeconfig as secret material and must not persist or log it.
|
||||
type TenantKubeconfig struct {
|
||||
Kubeconfig string
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// NewTenantBinding returns a tenant binding with production-safe default object names.
|
||||
func NewTenantBinding(namespace string) TenantBinding {
|
||||
return TenantBinding{
|
||||
Namespace: namespace,
|
||||
ServiceAccountName: DefaultTenantServiceAccountName,
|
||||
RoleBindingName: DefaultTenantRoleBindingName,
|
||||
ClusterRoleName: DefaultTenantClusterRoleName,
|
||||
ResourceQuotaName: DefaultTenantResourceQuotaName,
|
||||
Labels: map[string]string{
|
||||
"ocdp.io/managed-by": "ocdp",
|
||||
"ocdp.io/tenant": namespace,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// WithDefaults fills optional names while preserving explicit caller choices.
|
||||
func (b TenantBinding) WithDefaults() TenantBinding {
|
||||
if b.ServiceAccountName == "" {
|
||||
b.ServiceAccountName = DefaultTenantServiceAccountName
|
||||
}
|
||||
if b.RoleBindingName == "" {
|
||||
b.RoleBindingName = DefaultTenantRoleBindingName
|
||||
}
|
||||
if b.ClusterRoleName == "" {
|
||||
b.ClusterRoleName = DefaultTenantClusterRoleName
|
||||
}
|
||||
if b.ResourceQuotaName == "" {
|
||||
b.ResourceQuotaName = DefaultTenantResourceQuotaName
|
||||
}
|
||||
if b.Labels == nil {
|
||||
b.Labels = map[string]string{}
|
||||
}
|
||||
if b.Labels["ocdp.io/managed-by"] == "" {
|
||||
b.Labels["ocdp.io/managed-by"] = "ocdp"
|
||||
}
|
||||
if b.Namespace != "" && b.Labels["ocdp.io/tenant"] == "" {
|
||||
b.Labels["ocdp.io/tenant"] = b.Namespace
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Validate checks the object names required to provision a tenant namespace.
|
||||
func (b TenantBinding) Validate() error {
|
||||
b = b.WithDefaults()
|
||||
if strings.TrimSpace(b.Namespace) == "" || len(validation.IsDNS1123Label(b.Namespace)) > 0 {
|
||||
return ErrInvalidTenantNamespace
|
||||
}
|
||||
if strings.TrimSpace(b.ServiceAccountName) == "" || len(validation.IsDNS1123Subdomain(b.ServiceAccountName)) > 0 {
|
||||
return ErrInvalidTenantServiceAccount
|
||||
}
|
||||
if strings.TrimSpace(b.RoleBindingName) == "" || len(validation.IsDNS1123Subdomain(b.RoleBindingName)) > 0 {
|
||||
return ErrInvalidTenantRoleBinding
|
||||
}
|
||||
if strings.TrimSpace(b.ClusterRoleName) == "" || len(validation.IsDNS1123Subdomain(b.ClusterRoleName)) > 0 {
|
||||
return ErrInvalidTenantClusterRole
|
||||
}
|
||||
if strings.TrimSpace(b.ResourceQuotaName) == "" || len(validation.IsDNS1123Subdomain(b.ResourceQuotaName)) > 0 {
|
||||
return ErrInvalidTenantResourceQuota
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TenantTokenTTL caps requested kubeconfig lifetimes at MaxTenantKubeconfigTTL.
|
||||
func TenantTokenTTL(requested time.Duration) time.Duration {
|
||||
if requested <= 0 || requested > MaxTenantKubeconfigTTL {
|
||||
return MaxTenantKubeconfigTTL
|
||||
}
|
||||
return requested
|
||||
}
|
||||
|
||||
func (b TenantBinding) String() string {
|
||||
b = b.WithDefaults()
|
||||
return fmt.Sprintf("tenant namespace %q serviceAccount %q roleBinding %q", b.Namespace, b.ServiceAccountName, b.RoleBindingName)
|
||||
}
|
||||
Reference in New Issue
Block a user