Files
ocdp-go/backend/internal/domain/entity/tenant_binding.go
Ivan087 7f238a3168 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
2026-05-12 16:15:14 +08:00

124 lines
4.1 KiB
Go

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)
}