- 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
124 lines
4.1 KiB
Go
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)
|
|
}
|