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