From d812a0c7221baf61aec4ceea613ba094045e2820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20B=C3=A4hler?= Date: Tue, 2 Dec 2025 15:21:46 +0100 Subject: [PATCH] feat(tenant): add dedicated tenantowner crd (#1764) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Oliver Bähler --- PROJECT | 7 + api/v1beta2/resourcepool_types.go | 4 +- api/v1beta2/tenant_conversion_hub.go | 10 +- api/v1beta2/tenant_func.go | 61 ++ api/v1beta2/tenant_func_test.go | 30 +- api/v1beta2/tenant_status.go | 3 + api/v1beta2/tenant_types.go | 21 + api/v1beta2/tenantowner_types.go | 53 ++ api/v1beta2/zz_generated.deepcopy.go | 127 ++++- .../crds/capsule.clastix.io_tenantowners.yaml | 74 +++ .../crds/capsule.clastix.io_tenants.yaml | 88 +++ .../capsule/templates/crd-lifecycle/rbac.yaml | 1 + cmd/main.go | 2 +- e2e/additional_role_bindings_test.go | 8 +- e2e/administrators_test.go | 16 +- e2e/allowed_external_ips_test.go | 8 +- e2e/container_registry_test.go | 8 +- e2e/custom_capsule_group_test.go | 16 +- e2e/custom_resource_quota_test.go | 8 +- e2e/disable_externalname_test.go | 8 +- e2e/disable_ingress_wildcard_test.go | 8 +- e2e/disable_loadbalancer_test.go | 8 +- e2e/disable_node_ports_test.go | 8 +- e2e/dynamic_tenant_owner_clusterroles_test.go | 20 +- e2e/enable_loadbalancer_test.go | 8 +- e2e/enable_node_ports_test.go | 8 +- e2e/forbidden_annotations_regex_test.go | 8 +- e2e/force_tenant_prefix_tenant_scope_test.go | 16 +- e2e/force_tenant_prefix_test.go | 16 +- e2e/gateway_class_test.go | 24 +- e2e/globaltenantresource_test.go | 16 +- e2e/imagepullpolicy_multiple_test.go | 8 +- e2e/imagepullpolicy_single_test.go | 8 +- e2e/ingress_class_extensions_test.go | 8 +- e2e/ingress_class_networking_test.go | 16 +- ..._hostnames_collision_cluster_scope_test.go | 16 +- ...gress_hostnames_collision_disabled_test.go | 8 +- ...ostnames_collision_namespace_scope_test.go | 8 +- ...s_hostnames_collision_tenant_scope_test.go | 8 +- e2e/ingress_hostnames_test.go | 8 +- e2e/missing_tenant_test.go | 8 +- e2e/namespace_additional_metadata_test.go | 8 +- e2e/namespace_capsule_label_test.go | 8 +- e2e/namespace_hijacking_test.go | 16 +- e2e/namespace_metadata_controller_test.go | 8 +- e2e/namespace_metadata_webhook_test.go | 8 +- e2e/namespace_status_test.go | 8 +- e2e/namespace_user_metadata_test.go | 8 +- e2e/new_namespace_test.go | 24 +- e2e/node_user_metadata_test.go | 8 +- e2e/overquota_namespace_test.go | 8 +- e2e/owner_webhooks_test.go | 8 +- e2e/owners_test.go | 531 ++++++++++++++++++ e2e/pod_metadata_test.go | 8 +- e2e/pod_priority_class_test.go | 24 +- e2e/pod_runtime_class_test.go | 16 +- e2e/preventing_pv_cross_tenant_mount_test.go | 16 +- e2e/protected_namespace_regex_test.go | 8 +- e2e/resource_quota_exceeded_test.go | 8 +- e2e/resourcepool_test.go | 18 +- e2e/resourcepoolclaim_test.go | 17 +- e2e/sa_owner_promotion_test.go | 14 +- e2e/sa_prevent_privilege_escalation_test.go | 8 +- e2e/scalability_test.go | 8 +- e2e/selecting_non_owned_tenant_test.go | 8 +- e2e/selecting_tenant_fail_test.go | 32 +- e2e/selecting_tenant_with_label_test.go | 16 +- e2e/service_forbidden_metadata_test.go | 8 +- e2e/service_metadata_test.go | 8 +- e2e/storage_class_test.go | 24 +- e2e/tenant_cordoning_test.go | 8 +- e2e/tenant_metadata_test.go | 8 +- e2e/tenant_name_webhook_test.go | 8 +- e2e/tenant_protected_webhook_test.go | 8 +- e2e/tenant_resources_changes_test.go | 8 +- e2e/tenant_resources_test.go | 8 +- e2e/tenantresource_test.go | 8 +- e2e/utils_test.go | 61 ++ go.sum | 2 - internal/controllers/rbac/manager.go | 41 +- internal/controllers/tenant/manager.go | 137 ++++- internal/controllers/tenant/rolebindings.go | 40 +- internal/controllers/tenant/status.go | 112 +++- internal/controllers/tenant/utils.go | 102 ++++ internal/controllers/utils/predicates.go | 39 ++ .../webhook/namespace/mutation/handler.go | 7 +- .../webhook/namespace/validation/patch.go | 10 +- .../webhook/serviceaccounts/validating.go | 8 +- .../webhook/tenant/validation/warnings.go | 52 +- pkg/api/misc/selectors.go | 143 +++++ pkg/api/misc/zz_generated.deepcopy.go | 32 ++ pkg/api/owner.go | 15 +- pkg/api/owner_list.go | 10 +- pkg/api/owner_list_test.go | 48 +- pkg/api/owner_status_list.go | 136 +++++ pkg/api/owner_status_list_test.go | 355 ++++++++++++ .../rbac/const.go => pkg/api/rbac.go | 6 +- pkg/api/selectors.go | 49 -- pkg/api/users_list.go | 1 - pkg/api/zz_generated.deepcopy.go | 69 ++- pkg/utils/maps_test.go | 3 + pkg/utils/tenant/get_by.go | 7 +- pkg/utils/tenant/owned.go | 6 +- pkg/utils/users/is_tenant_owner.go | 24 +- pkg/utils/users/serviceaccounts.go | 2 +- 105 files changed, 2703 insertions(+), 543 deletions(-) create mode 100644 api/v1beta2/tenantowner_types.go create mode 100644 charts/capsule/crds/capsule.clastix.io_tenantowners.yaml create mode 100644 e2e/owners_test.go create mode 100644 pkg/api/misc/selectors.go create mode 100644 pkg/api/misc/zz_generated.deepcopy.go create mode 100644 pkg/api/owner_status_list.go create mode 100644 pkg/api/owner_status_list_test.go rename internal/controllers/rbac/const.go => pkg/api/rbac.go (89%) delete mode 100644 pkg/api/selectors.go diff --git a/PROJECT b/PROJECT index b8616a85..11b2b886 100644 --- a/PROJECT +++ b/PROJECT @@ -64,4 +64,11 @@ resources: kind: ResourcePoolClaim path: github.com/projectcapsule/capsule/api/v1beta2 version: v1beta2 +- api: + crdVersion: v1 + domain: clastix.io + group: capsule + kind: TenantOwner + path: github.com/projectcapsule/capsule/api/v1beta2 + version: v1beta2 version: "3" diff --git a/api/v1beta2/resourcepool_types.go b/api/v1beta2/resourcepool_types.go index 554c841b..bace7a6b 100644 --- a/api/v1beta2/resourcepool_types.go +++ b/api/v1beta2/resourcepool_types.go @@ -7,13 +7,13 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/projectcapsule/capsule/pkg/api" + "github.com/projectcapsule/capsule/pkg/api/misc" ) // ResourcePoolSpec. type ResourcePoolSpec struct { // Selector to match the namespaces that should be managed by the GlobalResourceQuota - Selectors []api.NamespaceSelector `json:"selectors,omitempty"` + Selectors []misc.NamespaceSelector `json:"selectors,omitempty"` // Define the resourcequota served by this resourcepool. Quota corev1.ResourceQuotaSpec `json:"quota"` // The Defaults given for each namespace, the default is not counted towards the total allocation diff --git a/api/v1beta2/tenant_conversion_hub.go b/api/v1beta2/tenant_conversion_hub.go index 28ddf2c8..c56c21b2 100644 --- a/api/v1beta2/tenant_conversion_hub.go +++ b/api/v1beta2/tenant_conversion_hub.go @@ -46,11 +46,13 @@ func (in *Tenant) ConvertFrom(raw conversion.Hub) error { } in.Spec.Owners = append(in.Spec.Owners, api.OwnerSpec{ - UserSpec: api.UserSpec{ - Kind: api.OwnerKind(owner.Kind), - Name: owner.Name, + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Kind: api.OwnerKind(owner.Kind), + Name: owner.Name, + }, + ClusterRoles: owner.GetRoles(*src, index), }, - ClusterRoles: owner.GetRoles(*src, index), ProxyOperations: proxySettings, }) } diff --git a/api/v1beta2/tenant_func.go b/api/v1beta2/tenant_func.go index be83b919..a9d1d27d 100644 --- a/api/v1beta2/tenant_func.go +++ b/api/v1beta2/tenant_func.go @@ -4,15 +4,76 @@ package v1beta2 import ( + "context" "slices" "sort" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apiserver/pkg/authentication/serviceaccount" + "sigs.k8s.io/controller-runtime/pkg/client" "github.com/projectcapsule/capsule/pkg/api" + "github.com/projectcapsule/capsule/pkg/api/meta" ) +func (in *Tenant) CollectOwners(ctx context.Context, c client.Client, allowPromotion bool, admins api.UserListSpec) (api.OwnerStatusListSpec, error) { + owners := in.Spec.Owners.ToStatusOwners() + + // Promoted ServiceAccounts + if allowPromotion && len(in.Status.Namespaces) > 0 { + saList := &corev1.ServiceAccountList{} + if err := c.List(ctx, saList, + client.MatchingLabels{ + meta.OwnerPromotionLabel: meta.OwnerPromotionLabelTrigger, + }, + ); err != nil { + return nil, err + } + + for _, sa := range saList.Items { + for _, ns := range in.Status.Namespaces { + if sa.GetNamespace() != ns { + continue + } + + owners.Upsert(api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Kind: api.ServiceAccountOwner, + Name: serviceaccount.ServiceAccountUsernamePrefix + sa.Namespace + ":" + sa.Name, + }, + ClusterRoles: []string{ + api.ProvisionerRoleName, + api.DeleterRoleName, + }, + }) + } + } + } + + // Administrators + for _, a := range admins { + owners.Upsert(api.CoreOwnerSpec{ + UserSpec: a, + ClusterRoles: []string{ + api.DeleterRoleName, + }, + }) + } + + // Dedicated Owner Objects + listed, err := in.Spec.Permissions.ListMatchingOwners(ctx, c) + if err != nil { + return nil, err + } + + for _, o := range listed { + owners.Upsert(o.Spec.CoreOwnerSpec) + } + + return owners, nil +} + func (in *Tenant) IsFull() bool { // we don't have limits on assigned Namespaces if in.Spec.NamespaceOptions == nil || in.Spec.NamespaceOptions.Quota == nil { diff --git a/api/v1beta2/tenant_func_test.go b/api/v1beta2/tenant_func_test.go index bbefd42f..30b76716 100644 --- a/api/v1beta2/tenant_func_test.go +++ b/api/v1beta2/tenant_func_test.go @@ -15,25 +15,31 @@ var tenant = &Tenant{ Spec: TenantSpec{ Owners: []api.OwnerSpec{ { - UserSpec: api.UserSpec{ - Kind: "User", - Name: "user1", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Kind: "User", + Name: "user1", + }, + ClusterRoles: []string{"cluster-admin", "read-only"}, }, - ClusterRoles: []string{"cluster-admin", "read-only"}, }, { - UserSpec: api.UserSpec{ - Kind: "Group", - Name: "group1", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Kind: "Group", + Name: "group1", + }, + ClusterRoles: []string{"edit"}, }, - ClusterRoles: []string{"edit"}, }, { - UserSpec: api.UserSpec{ - Kind: api.ServiceAccountOwner, - Name: "service", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Kind: api.ServiceAccountOwner, + Name: "service", + }, + ClusterRoles: []string{"read-only"}, }, - ClusterRoles: []string{"read-only"}, }, }, AdditionalRoleBindings: []api.AdditionalRoleBindingsSpec{ diff --git a/api/v1beta2/tenant_status.go b/api/v1beta2/tenant_status.go index 41c7e7e1..39900211 100644 --- a/api/v1beta2/tenant_status.go +++ b/api/v1beta2/tenant_status.go @@ -6,6 +6,7 @@ package v1beta2 import ( k8stypes "k8s.io/apimachinery/pkg/types" + "github.com/projectcapsule/capsule/pkg/api" "github.com/projectcapsule/capsule/pkg/api/meta" ) @@ -22,6 +23,8 @@ type TenantStatus struct { // Allowed Cluster Objects within Tenant TenantAvailableStatus `json:",inline"` + // Collected owners for this tenant + Owners api.OwnerStatusListSpec `json:"owners,omitempty"` // +kubebuilder:default=Active // The operational state of the Tenant. Possible values are "Active", "Cordoned". State tenantState `json:"state"` diff --git a/api/v1beta2/tenant_types.go b/api/v1beta2/tenant_types.go index b8b65173..f1434c80 100644 --- a/api/v1beta2/tenant_types.go +++ b/api/v1beta2/tenant_types.go @@ -4,13 +4,19 @@ package v1beta2 import ( + "context" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" "github.com/projectcapsule/capsule/pkg/api" + "github.com/projectcapsule/capsule/pkg/api/misc" ) // TenantSpec defines the desired state of Tenant. type TenantSpec struct { + // Specify Permissions for the Tenant. + Permissions Permissions `json:"permissions,omitempty"` // Specifies the owners of the Tenant. // Optional Owners api.OwnerListSpec `json:"owners,omitempty"` @@ -72,6 +78,21 @@ type TenantSpec struct { ForceTenantPrefix *bool `json:"forceTenantPrefix,omitempty"` } +type Permissions struct { + // Matches TenantOwner objects which are promoted to owners of this tenant + // The elements are OR operations and independent. You can see the resulting Tenant Owners + // in the Status.Owners specification of the Tenant. + MatchOwners []*metav1.LabelSelector `json:"matchOwners,omitempty"` +} + +func (p *Permissions) ListMatchingOwners( + ctx context.Context, + c client.Client, + opts ...client.ListOption, +) ([]*TenantOwner, error) { + return misc.ListBySelectors[*TenantOwner](ctx, c, &TenantOwnerList{}, p.MatchOwners) +} + // +kubebuilder:object:root=true // +kubebuilder:storageversion // +kubebuilder:subresource:status diff --git a/api/v1beta2/tenantowner_types.go b/api/v1beta2/tenantowner_types.go new file mode 100644 index 00000000..9714f1ca --- /dev/null +++ b/api/v1beta2/tenantowner_types.go @@ -0,0 +1,53 @@ +// Copyright 2020-2025 Project Capsule Authors +// SPDX-License-Identifier: Apache-2.0 + +package v1beta2 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/projectcapsule/capsule/pkg/api" +) + +// TenantOwnerSpec defines the desired state of TenantOwner. +type TenantOwnerSpec struct { + api.CoreOwnerSpec `json:",inline"` +} + +// TenantOwnerStatus defines the observed state of TenantOwner. +type TenantOwnerStatus struct{} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster + +// TenantOwner is the Schema for the tenantowners API. +type TenantOwner struct { + metav1.TypeMeta `json:",inline"` + + // metadata is a standard object metadata. + // +optional + metav1.ObjectMeta `json:"metadata,omitzero"` + + // spec defines the desired state of TenantOwner. + // +required + Spec TenantOwnerSpec `json:"spec"` + + // status defines the observed state of TenantOwner. + // +optional + Status TenantOwnerStatus `json:"status,omitzero"` +} + +// +kubebuilder:object:root=true + +// TenantOwnerList contains a list of TenantOwner. +type TenantOwnerList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitzero"` + + Items []TenantOwner `json:"items"` +} + +func init() { + SchemeBuilder.Register(&TenantOwner{}, &TenantOwnerList{}) +} diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index 461b73b3..29eb6de7 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -10,6 +10,7 @@ package v1beta2 import ( "github.com/projectcapsule/capsule/pkg/api" "github.com/projectcapsule/capsule/pkg/api/meta" + "github.com/projectcapsule/capsule/pkg/api/misc" corev1 "k8s.io/api/core/v1" "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -410,6 +411,32 @@ func (in *ObjectReferenceStatus) DeepCopy() *ObjectReferenceStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Permissions) DeepCopyInto(out *Permissions) { + *out = *in + if in.MatchOwners != nil { + in, out := &in.MatchOwners, &out.MatchOwners + *out = make([]*metav1.LabelSelector, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(metav1.LabelSelector) + (*in).DeepCopyInto(*out) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Permissions. +func (in *Permissions) DeepCopy() *Permissions { + if in == nil { + return nil + } + out := new(Permissions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in ProcessedItems) DeepCopyInto(out *ProcessedItems) { { @@ -727,7 +754,7 @@ func (in *ResourcePoolSpec) DeepCopyInto(out *ResourcePoolSpec) { *out = *in if in.Selectors != nil { in, out := &in.Selectors, &out.Selectors - *out = make([]api.NamespaceSelector, len(*in)) + *out = make([]misc.NamespaceSelector, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -982,6 +1009,96 @@ func (in *TenantList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TenantOwner) DeepCopyInto(out *TenantOwner) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantOwner. +func (in *TenantOwner) DeepCopy() *TenantOwner { + if in == nil { + return nil + } + out := new(TenantOwner) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TenantOwner) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TenantOwnerList) DeepCopyInto(out *TenantOwnerList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]TenantOwner, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantOwnerList. +func (in *TenantOwnerList) DeepCopy() *TenantOwnerList { + if in == nil { + return nil + } + out := new(TenantOwnerList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TenantOwnerList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TenantOwnerSpec) DeepCopyInto(out *TenantOwnerSpec) { + *out = *in + in.CoreOwnerSpec.DeepCopyInto(&out.CoreOwnerSpec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantOwnerSpec. +func (in *TenantOwnerSpec) DeepCopy() *TenantOwnerSpec { + if in == nil { + return nil + } + out := new(TenantOwnerSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TenantOwnerStatus) DeepCopyInto(out *TenantOwnerStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TenantOwnerStatus. +func (in *TenantOwnerStatus) DeepCopy() *TenantOwnerStatus { + if in == nil { + return nil + } + out := new(TenantOwnerStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TenantResource) DeepCopyInto(out *TenantResource) { *out = *in @@ -1092,6 +1209,7 @@ func (in *TenantResourceStatus) DeepCopy() *TenantResourceStatus { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TenantSpec) DeepCopyInto(out *TenantSpec) { *out = *in + in.Permissions.DeepCopyInto(&out.Permissions) if in.Owners != nil { in, out := &in.Owners, &out.Owners *out = make(api.OwnerListSpec, len(*in)) @@ -1179,6 +1297,13 @@ func (in *TenantSpec) DeepCopy() *TenantSpec { func (in *TenantStatus) DeepCopyInto(out *TenantStatus) { *out = *in in.TenantAvailableStatus.DeepCopyInto(&out.TenantAvailableStatus) + if in.Owners != nil { + in, out := &in.Owners, &out.Owners + *out = make(api.OwnerStatusListSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.Namespaces != nil { in, out := &in.Namespaces, &out.Namespaces *out = make([]string, len(*in)) diff --git a/charts/capsule/crds/capsule.clastix.io_tenantowners.yaml b/charts/capsule/crds/capsule.clastix.io_tenantowners.yaml new file mode 100644 index 00000000..4ee4a670 --- /dev/null +++ b/charts/capsule/crds/capsule.clastix.io_tenantowners.yaml @@ -0,0 +1,74 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: tenantowners.capsule.clastix.io +spec: + group: capsule.clastix.io + names: + kind: TenantOwner + listKind: TenantOwnerList + plural: tenantowners + singular: tenantowner + scope: Cluster + versions: + - name: v1beta2 + schema: + openAPIV3Schema: + description: TenantOwner is the Schema for the tenantowners API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: spec defines the desired state of TenantOwner. + properties: + clusterRoles: + default: + - admin + - capsule-namespace-deleter + description: Defines additional cluster-roles for the specific Owner. + items: + type: string + type: array + kind: + description: Kind of entity. Possible values are "User", "Group", + and "ServiceAccount" + enum: + - User + - Group + - ServiceAccount + type: string + name: + description: Name of the entity. + type: string + required: + - kind + - name + type: object + status: + description: status defines the observed state of TenantOwner. + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/capsule/crds/capsule.clastix.io_tenants.yaml b/charts/capsule/crds/capsule.clastix.io_tenants.yaml index f333b608..8e1a0164 100644 --- a/charts/capsule/crds/capsule.clastix.io_tenants.yaml +++ b/charts/capsule/crds/capsule.clastix.io_tenants.yaml @@ -2140,6 +2140,65 @@ spec: - name type: object type: array + permissions: + description: Specify Permissions for the Tenant. + properties: + matchOwners: + description: |- + Matches TenantOwner objects which are promoted to owners of this tenant + The elements are OR operations and independent. You can see the resulting Tenant Owners + in the Status.Owners specification of the Tenant. + items: + description: |- + A label selector is a label query over a set of resources. The result of matchLabels and + matchExpressions are ANDed. An empty label selector matches all objects. A null + label selector matches no objects. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: array + type: object podOptions: description: Specifies options for the Pods deployed in the Tenant namespaces, such as additional metadata. @@ -2596,6 +2655,35 @@ spec: items: type: string type: array + owners: + description: Collected owners for this tenant + items: + properties: + clusterRoles: + default: + - admin + - capsule-namespace-deleter + description: Defines additional cluster-roles for the specific + Owner. + items: + type: string + type: array + kind: + description: Kind of entity. Possible values are "User", "Group", + and "ServiceAccount" + enum: + - User + - Group + - ServiceAccount + type: string + name: + description: Name of the entity. + type: string + required: + - kind + - name + type: object + type: array size: description: How many namespaces are assigned to the Tenant. type: integer diff --git a/charts/capsule/templates/crd-lifecycle/rbac.yaml b/charts/capsule/templates/crd-lifecycle/rbac.yaml index 120f8870..b77989c6 100644 --- a/charts/capsule/templates/crd-lifecycle/rbac.yaml +++ b/charts/capsule/templates/crd-lifecycle/rbac.yaml @@ -31,6 +31,7 @@ rules: - tenantresources.capsule.clastix.io - globaltenantresources.capsule.clastix.io - tenants.capsule.clastix.io + - tenantowners.capsule.clastix.io verbs: - create - delete diff --git a/cmd/main.go b/cmd/main.go index 1aa4e152..96897c21 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -282,7 +282,7 @@ func main() { tenantvalidation.ServiceAccountNameHandler(), tenantvalidation.ForbiddenAnnotationsRegexHandler(), tenantvalidation.ProtectedHandler(), - tenantvalidation.WarningHandler(), + tenantvalidation.WarningHandler(cfg), ), route.NamespaceValidation( namespacevalidation.NamespaceHandler( diff --git a/e2e/additional_role_bindings_test.go b/e2e/additional_role_bindings_test.go index 1a1a1812..a29c262b 100644 --- a/e2e/additional_role_bindings_test.go +++ b/e2e/additional_role_bindings_test.go @@ -24,9 +24,11 @@ var _ = Describe("creating a Namespace with an additional Role Binding", Label(" Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "dale", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "dale", + Kind: "User", + }, }, }, }, diff --git a/e2e/administrators_test.go b/e2e/administrators_test.go index 649d2a8a..840ed4a0 100644 --- a/e2e/administrators_test.go +++ b/e2e/administrators_test.go @@ -27,9 +27,11 @@ var _ = Describe("Administrators", Label("namespace", "permissions"), func() { Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "paul", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "paul", + Kind: "User", + }, }, }, }, @@ -43,9 +45,11 @@ var _ = Describe("Administrators", Label("namespace", "permissions"), func() { Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "george", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "george", + Kind: "User", + }, }, }, }, diff --git a/e2e/allowed_external_ips_test.go b/e2e/allowed_external_ips_test.go index b6b5399b..46afe15c 100644 --- a/e2e/allowed_external_ips_test.go +++ b/e2e/allowed_external_ips_test.go @@ -24,9 +24,11 @@ var _ = Describe("enforcing an allowed set of Service external IPs", Label("tena Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "google", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "google", + Kind: "User", + }, }, }, }, diff --git a/e2e/container_registry_test.go b/e2e/container_registry_test.go index b3e8bfb6..3a62608a 100644 --- a/e2e/container_registry_test.go +++ b/e2e/container_registry_test.go @@ -35,9 +35,11 @@ var _ = Describe("enforcing a Container Registry", Label("tenant", "images", "re Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "matt", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "matt", + Kind: "User", + }, }, }, }, diff --git a/e2e/custom_capsule_group_test.go b/e2e/custom_capsule_group_test.go index adf34266..45a22090 100644 --- a/e2e/custom_capsule_group_test.go +++ b/e2e/custom_capsule_group_test.go @@ -25,15 +25,19 @@ var _ = Describe("creating a Namespace as Tenant owner with custom --capsule-gro Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "alice", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "alice", + Kind: "User", + }, }, }, { - UserSpec: api.UserSpec{ - Name: "bob", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "bob", + Kind: "User", + }, }, }, }, diff --git a/e2e/custom_resource_quota_test.go b/e2e/custom_resource_quota_test.go index ff4d1dca..245681f0 100644 --- a/e2e/custom_resource_quota_test.go +++ b/e2e/custom_resource_quota_test.go @@ -33,9 +33,11 @@ var _ = Describe("when Tenant limits custom Resource Quota", Label("resourcequot Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "resource", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "resource", + Kind: "User", + }, }, }, }, diff --git a/e2e/disable_externalname_test.go b/e2e/disable_externalname_test.go index 32fde23a..ef5e38a0 100644 --- a/e2e/disable_externalname_test.go +++ b/e2e/disable_externalname_test.go @@ -25,9 +25,11 @@ var _ = Describe("creating an ExternalName service when it is disabled for Tenan Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "google", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "google", + Kind: "User", + }, }, }, }, diff --git a/e2e/disable_ingress_wildcard_test.go b/e2e/disable_ingress_wildcard_test.go index 4b80491c..55fb67ef 100644 --- a/e2e/disable_ingress_wildcard_test.go +++ b/e2e/disable_ingress_wildcard_test.go @@ -31,9 +31,11 @@ var _ = Describe("creating an Ingress with a wildcard when it is denied for the Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "scott", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "scott", + Kind: "User", + }, }, }, }, diff --git a/e2e/disable_loadbalancer_test.go b/e2e/disable_loadbalancer_test.go index 76916924..70df28dc 100644 --- a/e2e/disable_loadbalancer_test.go +++ b/e2e/disable_loadbalancer_test.go @@ -25,9 +25,11 @@ var _ = Describe("creating a LoadBalancer service when it is disabled for Tenant Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "amazon", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "amazon", + Kind: "User", + }, }, }, }, diff --git a/e2e/disable_node_ports_test.go b/e2e/disable_node_ports_test.go index 0e288af5..d70e14e4 100644 --- a/e2e/disable_node_ports_test.go +++ b/e2e/disable_node_ports_test.go @@ -25,9 +25,11 @@ var _ = Describe("creating a nodePort service when it is disabled for Tenant", L Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "google", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "google", + Kind: "User", + }, }, }, }, diff --git a/e2e/dynamic_tenant_owner_clusterroles_test.go b/e2e/dynamic_tenant_owner_clusterroles_test.go index 3e0f8a09..c6e99caa 100644 --- a/e2e/dynamic_tenant_owner_clusterroles_test.go +++ b/e2e/dynamic_tenant_owner_clusterroles_test.go @@ -22,18 +22,22 @@ var _ = Describe("defining dynamic Tenant Owner Cluster Roles", Label("tenant"), Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Kind: "User", - Name: "michonne", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Kind: "User", + Name: "michonne", + }, + ClusterRoles: []string{"editor", "manager"}, }, - ClusterRoles: []string{"editor", "manager"}, }, { - UserSpec: api.UserSpec{ - Name: "kingdom", - Kind: "Group", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "kingdom", + Kind: "Group", + }, + ClusterRoles: []string{"readonly"}, }, - ClusterRoles: []string{"readonly"}, }, }, }, diff --git a/e2e/enable_loadbalancer_test.go b/e2e/enable_loadbalancer_test.go index d7ab6201..cb0fcca8 100644 --- a/e2e/enable_loadbalancer_test.go +++ b/e2e/enable_loadbalancer_test.go @@ -25,9 +25,11 @@ var _ = Describe("creating a LoadBalancer service when it is enabled for Tenant" Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "netflix", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "netflix", + Kind: "User", + }, }, }, }, diff --git a/e2e/enable_node_ports_test.go b/e2e/enable_node_ports_test.go index ec6fe631..523a1d01 100644 --- a/e2e/enable_node_ports_test.go +++ b/e2e/enable_node_ports_test.go @@ -24,9 +24,11 @@ var _ = Describe("creating a nodePort service when it is enabled for Tenant", La Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "google", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "google", + Kind: "User", + }, }, }, }, diff --git a/e2e/forbidden_annotations_regex_test.go b/e2e/forbidden_annotations_regex_test.go index c5577535..5e6a9535 100644 --- a/e2e/forbidden_annotations_regex_test.go +++ b/e2e/forbidden_annotations_regex_test.go @@ -68,9 +68,11 @@ var _ = Describe("creating a tenant with various forbidden regexes", Label("tena Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "alice", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "alice", + Kind: "User", + }, }, }, }, diff --git a/e2e/force_tenant_prefix_tenant_scope_test.go b/e2e/force_tenant_prefix_tenant_scope_test.go index 3462a488..3bfd1116 100644 --- a/e2e/force_tenant_prefix_tenant_scope_test.go +++ b/e2e/force_tenant_prefix_tenant_scope_test.go @@ -23,9 +23,11 @@ var _ = Describe("creating a Namespace with Tenant name prefix enforcement at Te ForceTenantPrefix: &[]bool{true}[0], Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "john", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "john", + Kind: "User", + }, }, }, }, @@ -39,9 +41,11 @@ var _ = Describe("creating a Namespace with Tenant name prefix enforcement at Te ForceTenantPrefix: &[]bool{false}[0], Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "john", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "john", + Kind: "User", + }, }, }, }, diff --git a/e2e/force_tenant_prefix_test.go b/e2e/force_tenant_prefix_test.go index 62a4d4d5..5b980d4a 100644 --- a/e2e/force_tenant_prefix_test.go +++ b/e2e/force_tenant_prefix_test.go @@ -22,9 +22,11 @@ var _ = Describe("creating a Namespace with Tenant name prefix enforcement", Lab Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "john", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "john", + Kind: "User", + }, }, }, }, @@ -37,9 +39,11 @@ var _ = Describe("creating a Namespace with Tenant name prefix enforcement", Lab Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "john", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "john", + Kind: "User", + }, }, }, }, diff --git a/e2e/gateway_class_test.go b/e2e/gateway_class_test.go index 72b0dcd0..b770237c 100644 --- a/e2e/gateway_class_test.go +++ b/e2e/gateway_class_test.go @@ -79,9 +79,11 @@ var _ = Describe("when Tenant handles Gateway classes", Label("tenant", "classes Spec: capsulev1beta2.TenantSpec{ Owners: []api.OwnerSpec{ { - UserSpec: api.UserSpec{ - Name: "gateway-default-and-label-selector", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "gateway-default-and-label-selector", + Kind: "User", + }, }, }, }, @@ -110,9 +112,11 @@ var _ = Describe("when Tenant handles Gateway classes", Label("tenant", "classes Spec: capsulev1beta2.TenantSpec{ Owners: []api.OwnerSpec{ { - UserSpec: api.UserSpec{ - Name: "gateway-with-label-selector-only", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "gateway-with-label-selector-only", + Kind: "User", + }, }, }, }, @@ -140,9 +144,11 @@ var _ = Describe("when Tenant handles Gateway classes", Label("tenant", "classes Spec: capsulev1beta2.TenantSpec{ Owners: []api.OwnerSpec{ { - UserSpec: api.UserSpec{ - Name: "e2e-gateway-no-restrictions", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "e2e-gateway-no-restrictions", + Kind: "User", + }, }, }, }, diff --git a/e2e/globaltenantresource_test.go b/e2e/globaltenantresource_test.go index eaecad1f..5de9a2bd 100644 --- a/e2e/globaltenantresource_test.go +++ b/e2e/globaltenantresource_test.go @@ -34,9 +34,11 @@ var _ = Describe("Creating a GlobalTenantResource object", func() { Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "solar-user", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "solar-user", + Kind: "User", + }, }, }, }, @@ -53,9 +55,11 @@ var _ = Describe("Creating a GlobalTenantResource object", func() { Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "wind-user", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "wind-user", + Kind: "User", + }, }, }, }, diff --git a/e2e/imagepullpolicy_multiple_test.go b/e2e/imagepullpolicy_multiple_test.go index f9cefb89..e1ee951b 100644 --- a/e2e/imagepullpolicy_multiple_test.go +++ b/e2e/imagepullpolicy_multiple_test.go @@ -24,9 +24,11 @@ var _ = Describe("enforcing some defined ImagePullPolicy", Label("tenant", "imag Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "alex", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "alex", + Kind: "User", + }, }, }, }, diff --git a/e2e/imagepullpolicy_single_test.go b/e2e/imagepullpolicy_single_test.go index 7cd7515b..2ff78185 100644 --- a/e2e/imagepullpolicy_single_test.go +++ b/e2e/imagepullpolicy_single_test.go @@ -24,9 +24,11 @@ var _ = Describe("enforcing a defined ImagePullPolicy", Label("tenant", "images" Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "axel", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "axel", + Kind: "User", + }, }, }, }, diff --git a/e2e/ingress_class_extensions_test.go b/e2e/ingress_class_extensions_test.go index 45c01e64..c184dc15 100644 --- a/e2e/ingress_class_extensions_test.go +++ b/e2e/ingress_class_extensions_test.go @@ -27,9 +27,11 @@ var _ = Describe("when Tenant handles Ingress classes with extensions/v1beta1", Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "ingress", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "ingress", + Kind: "User", + }, }, }, }, diff --git a/e2e/ingress_class_networking_test.go b/e2e/ingress_class_networking_test.go index 822920a5..77d7d317 100644 --- a/e2e/ingress_class_networking_test.go +++ b/e2e/ingress_class_networking_test.go @@ -32,9 +32,11 @@ var _ = Describe("when Tenant handles Ingress classes with networking.k8s.io/v1" Spec: capsulev1beta2.TenantSpec{ Owners: []api.OwnerSpec{ { - UserSpec: api.UserSpec{ - Name: "ingress-selector", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "ingress-selector", + Kind: "User", + }, }, }, }, @@ -63,9 +65,11 @@ var _ = Describe("when Tenant handles Ingress classes with networking.k8s.io/v1" Spec: capsulev1beta2.TenantSpec{ Owners: []api.OwnerSpec{ { - UserSpec: api.UserSpec{ - Name: "ingress-default", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "ingress-default", + Kind: "User", + }, }, }, }, diff --git a/e2e/ingress_hostnames_collision_cluster_scope_test.go b/e2e/ingress_hostnames_collision_cluster_scope_test.go index 59c188ec..5c921e41 100644 --- a/e2e/ingress_hostnames_collision_cluster_scope_test.go +++ b/e2e/ingress_hostnames_collision_cluster_scope_test.go @@ -27,9 +27,11 @@ var _ = Describe("when handling Cluster scoped Ingress hostnames collision", Lab Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "ingress-tenant-one", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "ingress-tenant-one", + Kind: "User", + }, }, }, }, @@ -45,9 +47,11 @@ var _ = Describe("when handling Cluster scoped Ingress hostnames collision", Lab Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "ingress-tenant-two", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "ingress-tenant-two", + Kind: "User", + }, }, }, }, diff --git a/e2e/ingress_hostnames_collision_disabled_test.go b/e2e/ingress_hostnames_collision_disabled_test.go index c6b31384..7526577c 100644 --- a/e2e/ingress_hostnames_collision_disabled_test.go +++ b/e2e/ingress_hostnames_collision_disabled_test.go @@ -27,9 +27,11 @@ var _ = Describe("when disabling Ingress hostnames collision", Label("ingress"), Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "ingress-disabled", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "ingress-disabled", + Kind: "User", + }, }, }, }, diff --git a/e2e/ingress_hostnames_collision_namespace_scope_test.go b/e2e/ingress_hostnames_collision_namespace_scope_test.go index b5fce227..792c5c47 100644 --- a/e2e/ingress_hostnames_collision_namespace_scope_test.go +++ b/e2e/ingress_hostnames_collision_namespace_scope_test.go @@ -27,9 +27,11 @@ var _ = Describe("when handling Namespace scoped Ingress hostnames collision", L Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "ingress-namespace", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "ingress-namespace", + Kind: "User", + }, }, }, }, diff --git a/e2e/ingress_hostnames_collision_tenant_scope_test.go b/e2e/ingress_hostnames_collision_tenant_scope_test.go index 0dd2c520..6812b79a 100644 --- a/e2e/ingress_hostnames_collision_tenant_scope_test.go +++ b/e2e/ingress_hostnames_collision_tenant_scope_test.go @@ -27,9 +27,11 @@ var _ = Describe("when handling Tenant scoped Ingress hostnames collision", Labe Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "ingress-tenant", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "ingress-tenant", + Kind: "User", + }, }, }, }, diff --git a/e2e/ingress_hostnames_test.go b/e2e/ingress_hostnames_test.go index af105c2f..732a34bd 100644 --- a/e2e/ingress_hostnames_test.go +++ b/e2e/ingress_hostnames_test.go @@ -27,9 +27,11 @@ var _ = Describe("when Tenant handles Ingress hostnames", Label("ingress"), func Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "hostname", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "hostname", + Kind: "User", + }, }, }, }, diff --git a/e2e/missing_tenant_test.go b/e2e/missing_tenant_test.go index b9e3e938..f1e532be 100644 --- a/e2e/missing_tenant_test.go +++ b/e2e/missing_tenant_test.go @@ -20,9 +20,11 @@ var _ = Describe("creating a Namespace creation with no Tenant assigned", Label( Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "missing", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "missing", + Kind: "User", + }, }, }, }, diff --git a/e2e/namespace_additional_metadata_test.go b/e2e/namespace_additional_metadata_test.go index 033515c3..835ffa13 100644 --- a/e2e/namespace_additional_metadata_test.go +++ b/e2e/namespace_additional_metadata_test.go @@ -34,9 +34,11 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", L Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "gatsby", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "gatsby", + Kind: "User", + }, }, }, }, diff --git a/e2e/namespace_capsule_label_test.go b/e2e/namespace_capsule_label_test.go index da18d448..d69a6c12 100644 --- a/e2e/namespace_capsule_label_test.go +++ b/e2e/namespace_capsule_label_test.go @@ -25,9 +25,11 @@ var _ = Describe("creating several Namespaces for a Tenant", Label("namespace"), Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "charlie", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "charlie", + Kind: "User", + }, }, }, }, diff --git a/e2e/namespace_hijacking_test.go b/e2e/namespace_hijacking_test.go index b1c95992..39ff0a9f 100644 --- a/e2e/namespace_hijacking_test.go +++ b/e2e/namespace_hijacking_test.go @@ -26,15 +26,19 @@ var _ = Describe("creating several Namespaces for a Tenant", Label("namespace", Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "gatsby", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "gatsby", + Kind: "User", + }, }, }, { - UserSpec: api.UserSpec{ - Kind: "ServiceAccount", - Name: "system:serviceaccount:attacker-system:attacker", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Kind: "ServiceAccount", + Name: "system:serviceaccount:attacker-system:attacker", + }, }, }, }, diff --git a/e2e/namespace_metadata_controller_test.go b/e2e/namespace_metadata_controller_test.go index fce8fb21..b43c015c 100644 --- a/e2e/namespace_metadata_controller_test.go +++ b/e2e/namespace_metadata_controller_test.go @@ -31,9 +31,11 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", L Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "gatsby", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "gatsby", + Kind: "User", + }, }, }, }, diff --git a/e2e/namespace_metadata_webhook_test.go b/e2e/namespace_metadata_webhook_test.go index 5aba0f22..bc5e06ac 100644 --- a/e2e/namespace_metadata_webhook_test.go +++ b/e2e/namespace_metadata_webhook_test.go @@ -31,9 +31,11 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", L Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "gatsby", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "gatsby", + Kind: "User", + }, }, }, }, diff --git a/e2e/namespace_status_test.go b/e2e/namespace_status_test.go index 768cc341..b8b3b02d 100644 --- a/e2e/namespace_status_test.go +++ b/e2e/namespace_status_test.go @@ -24,9 +24,11 @@ var _ = Describe("creating namespace with status lifecycle", Label("namespace", Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "gatsby", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "gatsby", + Kind: "User", + }, }, }, }, diff --git a/e2e/namespace_user_metadata_test.go b/e2e/namespace_user_metadata_test.go index e2c72ebe..c9e92d7a 100644 --- a/e2e/namespace_user_metadata_test.go +++ b/e2e/namespace_user_metadata_test.go @@ -35,9 +35,11 @@ var _ = Describe("creating a Namespace with user-specified labels and annotation }, Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "gatsby", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "gatsby", + Kind: "User", + }, }, }, }, diff --git a/e2e/new_namespace_test.go b/e2e/new_namespace_test.go index f4c4c352..14119ce6 100644 --- a/e2e/new_namespace_test.go +++ b/e2e/new_namespace_test.go @@ -22,21 +22,27 @@ var _ = Describe("creating a Namespaces as different type of Tenant owners", Lab Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "alice", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "alice", + Kind: "User", + }, }, }, { - UserSpec: api.UserSpec{ - Name: "bob", - Kind: "Group", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "bob", + Kind: "Group", + }, }, }, { - UserSpec: api.UserSpec{ - Name: "system:serviceaccount:new-namespace-sa:default", - Kind: "ServiceAccount", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "system:serviceaccount:new-namespace-sa:default", + Kind: "ServiceAccount", + }, }, }, }, diff --git a/e2e/node_user_metadata_test.go b/e2e/node_user_metadata_test.go index 1d78da81..243dac96 100644 --- a/e2e/node_user_metadata_test.go +++ b/e2e/node_user_metadata_test.go @@ -29,9 +29,11 @@ var _ = Describe("modifying node labels and annotations", Label("config", "nodes Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "gatsby", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "gatsby", + Kind: "User", + }, }, }, }, diff --git a/e2e/overquota_namespace_test.go b/e2e/overquota_namespace_test.go index 20b44ec1..06434bdd 100644 --- a/e2e/overquota_namespace_test.go +++ b/e2e/overquota_namespace_test.go @@ -23,9 +23,11 @@ var _ = Describe("creating a Namespace in over-quota of three", Label("namespace Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "bob", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "bob", + Kind: "User", + }, }, }, }, diff --git a/e2e/owner_webhooks_test.go b/e2e/owner_webhooks_test.go index 9a56d775..69e96a18 100644 --- a/e2e/owner_webhooks_test.go +++ b/e2e/owner_webhooks_test.go @@ -27,9 +27,11 @@ var _ = Describe("when Tenant owner interacts with the webhooks", Label("tenant" Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "ruby", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "ruby", + Kind: "User", + }, }, }, }, diff --git a/e2e/owners_test.go b/e2e/owners_test.go new file mode 100644 index 00000000..fc41b49b --- /dev/null +++ b/e2e/owners_test.go @@ -0,0 +1,531 @@ +// Copyright 2020-2023 Project Capsule Authors. +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" + "github.com/projectcapsule/capsule/pkg/api" +) + +var _ = Describe("Owners", Label("tenant", "permissions", "owners"), func() { + originConfig := &capsulev1beta2.CapsuleConfiguration{} + + tnt1 := &capsulev1beta2.Tenant{ + ObjectMeta: metav1.ObjectMeta{ + Name: "e2e-owners-1", + }, + Spec: capsulev1beta2.TenantSpec{ + Permissions: capsulev1beta2.Permissions{ + MatchOwners: []*metav1.LabelSelector{ + { + MatchLabels: map[string]string{ + "customer": "x", + }, + }, + { + MatchLabels: map[string]string{ + "team": "devops", + }, + }, + }, + }, + Owners: api.OwnerListSpec{ + { + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "e2e-owners-1", + Kind: "User", + }, + }, + }, + { + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "e2e-owners-1-group", + Kind: "Group", + }, + }, + }, + { + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "system:serviceaccount:capsule-system:capsule", + Kind: "ServiceAccount", + }, + }, + }, + }, + }, + } + + tnt2 := &capsulev1beta2.Tenant{ + ObjectMeta: metav1.ObjectMeta{ + Name: "e2e-owners-2", + }, + Spec: capsulev1beta2.TenantSpec{ + Permissions: capsulev1beta2.Permissions{ + MatchOwners: []*metav1.LabelSelector{ + { + MatchLabels: map[string]string{ + "customer": "x", + }, + }, + { + MatchLabels: map[string]string{ + "team": "infrastructure", + }, + }, + }, + }, + Owners: api.OwnerListSpec{ + { + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "e2e-owners-2", + Kind: "User", + }, + }, + }, + { + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "e2e-owners-2-group", + Kind: "Group", + }, + }, + }, + { + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "system:serviceaccount:capsule-system:capsule", + Kind: "ServiceAccount", + }, + }, + }, + }, + }, + } + + ownersInfra := &capsulev1beta2.TenantOwner{ + ObjectMeta: metav1.ObjectMeta{ + Name: "e2e-owners-infra", + Labels: map[string]string{ + "team": "infrastructure", + }, + }, + Spec: capsulev1beta2.TenantOwnerSpec{ + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Kind: api.GroupOwner, + Name: "oidc:comp:administrators", + }, + ClusterRoles: []string{ + "mega-admin", + }, + }, + }, + } + + ownersDevops := &capsulev1beta2.TenantOwner{ + ObjectMeta: metav1.ObjectMeta{ + Name: "e2e-owners-devops", + Labels: map[string]string{ + "team": "devops", + }, + }, + Spec: capsulev1beta2.TenantOwnerSpec{ + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Kind: api.GroupOwner, + Name: "oidc:comp:devops", + }, + ClusterRoles: []string{ + "namespaced-admin", + }, + }, + }, + } + + ownersCommon := &capsulev1beta2.TenantOwner{ + ObjectMeta: metav1.ObjectMeta{ + Name: "e2e-owners-common", + Labels: map[string]string{ + "team": "infrastructure", + "customer": "x", + }, + }, + Spec: capsulev1beta2.TenantOwnerSpec{ + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Kind: api.ServiceAccountOwner, + Name: "system:serviceaccount:capsule-system:capsule", + }, + ClusterRoles: []string{ + "service-admin", + }, + }, + }, + } + + JustBeforeEach(func() { + Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: defaultConfigurationName}, originConfig)).To(Succeed()) + + for _, tnt := range []*capsulev1beta2.Tenant{tnt1, tnt2} { + EventuallyCreation(func() error { + tnt.ResourceVersion = "" + + return k8sClient.Create(context.TODO(), tnt) + }).Should(Succeed()) + } + + for _, tnt := range []*capsulev1beta2.TenantOwner{ownersInfra, ownersDevops, ownersCommon} { + EventuallyCreation(func() error { + tnt.ResourceVersion = "" + + return k8sClient.Create(context.TODO(), tnt) + }).Should(Succeed()) + } + }) + + JustAfterEach(func() { + for _, tnt := range []*capsulev1beta2.Tenant{tnt1, tnt2} { + err := k8sClient.Delete(context.TODO(), tnt) + Expect(client.IgnoreNotFound(err)).To(Succeed()) + } + + for _, owners := range []*capsulev1beta2.TenantOwner{ownersInfra, ownersDevops, ownersCommon} { + err := k8sClient.Delete(context.TODO(), owners) + Expect(client.IgnoreNotFound(err)).To(Succeed()) + } + }) + + It("Verify owners for", func() { + By("checking owners (e2e-owners-1)", func() { + t := &capsulev1beta2.Tenant{} + Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt1.GetName()}, t)).Should(Succeed()) + + expectedOwners := api.OwnerStatusListSpec{ + { + UserSpec: api.UserSpec{ + Kind: api.GroupOwner, + Name: "e2e-owners-1-group", + }, + ClusterRoles: []string{"admin", "capsule-namespace-deleter"}, + }, + { + UserSpec: api.UserSpec{ + Kind: api.GroupOwner, + Name: "oidc:comp:devops", + }, + ClusterRoles: []string{"namespaced-admin"}, + }, + { + UserSpec: api.UserSpec{ + Kind: api.ServiceAccountOwner, + Name: "system:serviceaccount:capsule-system:capsule", + }, + ClusterRoles: []string{"admin", "capsule-namespace-deleter", "service-admin"}, + }, + { + UserSpec: api.UserSpec{ + Kind: api.UserOwner, + Name: "e2e-owners-1", + }, + ClusterRoles: []string{"admin", "capsule-namespace-deleter"}, + }, + } + + Expect(normalizeOwners(t.Status.Owners)). + To(Equal(normalizeOwners(expectedOwners))) + + VerifyTenantRoleBindings(t) + }) + + By("checking owners (e2e-owners-2)", func() { + t := &capsulev1beta2.Tenant{} + Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt2.GetName()}, t)).Should(Succeed()) + + expectedOwners := api.OwnerStatusListSpec{ + { + UserSpec: api.UserSpec{ + Kind: api.GroupOwner, + Name: "e2e-owners-2-group", + }, + ClusterRoles: []string{"admin", "capsule-namespace-deleter"}, + }, + { + UserSpec: api.UserSpec{ + Kind: api.GroupOwner, + Name: "oidc:comp:administrators", + }, + ClusterRoles: []string{"mega-admin"}, + }, + { + UserSpec: api.UserSpec{ + Kind: api.ServiceAccountOwner, + Name: "system:serviceaccount:capsule-system:capsule", + }, + ClusterRoles: []string{"admin", "capsule-namespace-deleter", "service-admin"}, + }, + { + UserSpec: api.UserSpec{ + Kind: api.UserOwner, + Name: "e2e-owners-2", + }, + ClusterRoles: []string{"admin", "capsule-namespace-deleter"}, + }, + } + + Expect(normalizeOwners(t.Status.Owners)). + To(Equal(normalizeOwners(expectedOwners))) + + VerifyTenantRoleBindings(t) + }) + + By("remove common tenant-owners", func() { + Expect(k8sClient.Delete(context.TODO(), ownersCommon)).Should(Succeed()) + }) + + By("checking owners (e2e-owners-1)", func() { + t := &capsulev1beta2.Tenant{} + Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt1.GetName()}, t)).Should(Succeed()) + + expectedOwners := api.OwnerStatusListSpec{ + { + UserSpec: api.UserSpec{ + Kind: api.GroupOwner, + Name: "e2e-owners-1-group", + }, + ClusterRoles: []string{"admin", "capsule-namespace-deleter"}, + }, + { + UserSpec: api.UserSpec{ + Kind: api.GroupOwner, + Name: "oidc:comp:devops", + }, + ClusterRoles: []string{"namespaced-admin"}, + }, + { + UserSpec: api.UserSpec{ + Kind: api.ServiceAccountOwner, + Name: "system:serviceaccount:capsule-system:capsule", + }, + ClusterRoles: []string{"admin", "capsule-namespace-deleter"}, + }, + { + UserSpec: api.UserSpec{ + Kind: api.UserOwner, + Name: "e2e-owners-1", + }, + ClusterRoles: []string{"admin", "capsule-namespace-deleter"}, + }, + } + + Expect(normalizeOwners(t.Status.Owners)). + To(Equal(normalizeOwners(expectedOwners))) + + VerifyTenantRoleBindings(t) + }) + + By("checking owners (e2e-owners-2)", func() { + t := &capsulev1beta2.Tenant{} + Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt2.GetName()}, t)).Should(Succeed()) + + expectedOwners := api.OwnerStatusListSpec{ + { + UserSpec: api.UserSpec{ + Kind: api.GroupOwner, + Name: "e2e-owners-2-group", + }, + ClusterRoles: []string{"admin", "capsule-namespace-deleter"}, + }, + { + UserSpec: api.UserSpec{ + Kind: api.GroupOwner, + Name: "oidc:comp:administrators", + }, + ClusterRoles: []string{"mega-admin"}, + }, + { + UserSpec: api.UserSpec{ + Kind: api.ServiceAccountOwner, + Name: "system:serviceaccount:capsule-system:capsule", + }, + ClusterRoles: []string{"admin", "capsule-namespace-deleter"}, + }, + { + UserSpec: api.UserSpec{ + Kind: api.UserOwner, + Name: "e2e-owners-2", + }, + ClusterRoles: []string{"admin", "capsule-namespace-deleter"}, + }, + } + + Expect(normalizeOwners(t.Status.Owners)). + To(Equal(normalizeOwners(expectedOwners))) + + VerifyTenantRoleBindings(t) + }) + + By("remove admin tenant-owners", func() { + Expect(k8sClient.Delete(context.TODO(), ownersInfra)).Should(Succeed()) + }) + + By("checking owners (e2e-owners-1)", func() { + t := &capsulev1beta2.Tenant{} + Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt1.GetName()}, t)).Should(Succeed()) + + expectedOwners := api.OwnerStatusListSpec{ + { + UserSpec: api.UserSpec{ + Kind: api.GroupOwner, + Name: "e2e-owners-1-group", + }, + ClusterRoles: []string{"admin", "capsule-namespace-deleter"}, + }, + { + UserSpec: api.UserSpec{ + Kind: api.GroupOwner, + Name: "oidc:comp:devops", + }, + ClusterRoles: []string{"namespaced-admin"}, + }, + { + UserSpec: api.UserSpec{ + Kind: api.ServiceAccountOwner, + Name: "system:serviceaccount:capsule-system:capsule", + }, + ClusterRoles: []string{"admin", "capsule-namespace-deleter"}, + }, + { + UserSpec: api.UserSpec{ + Kind: api.UserOwner, + Name: "e2e-owners-1", + }, + ClusterRoles: []string{"admin", "capsule-namespace-deleter"}, + }, + } + + Expect(normalizeOwners(t.Status.Owners)). + To(Equal(normalizeOwners(expectedOwners))) + + VerifyTenantRoleBindings(t) + }) + + By("checking owners (e2e-owners-2)", func() { + t := &capsulev1beta2.Tenant{} + Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt2.GetName()}, t)).Should(Succeed()) + + expectedOwners := api.OwnerStatusListSpec{ + { + UserSpec: api.UserSpec{ + Kind: api.GroupOwner, + Name: "e2e-owners-2-group", + }, + ClusterRoles: []string{"admin", "capsule-namespace-deleter"}, + }, + { + UserSpec: api.UserSpec{ + Kind: api.ServiceAccountOwner, + Name: "system:serviceaccount:capsule-system:capsule", + }, + ClusterRoles: []string{"admin", "capsule-namespace-deleter"}, + }, + { + UserSpec: api.UserSpec{ + Kind: api.UserOwner, + Name: "e2e-owners-2", + }, + ClusterRoles: []string{"admin", "capsule-namespace-deleter"}, + }, + } + + Expect(normalizeOwners(t.Status.Owners)). + To(Equal(normalizeOwners(expectedOwners))) + + VerifyTenantRoleBindings(t) + }) + + By("remove admin tenant-owners", func() { + Expect(k8sClient.Delete(context.TODO(), ownersDevops)).Should(Succeed()) + }) + + By("checking owners (e2e-owners-1)", func() { + t := &capsulev1beta2.Tenant{} + Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt1.GetName()}, t)).Should(Succeed()) + + expectedOwners := api.OwnerStatusListSpec{ + { + UserSpec: api.UserSpec{ + Kind: api.GroupOwner, + Name: "e2e-owners-1-group", + }, + ClusterRoles: []string{"admin", "capsule-namespace-deleter"}, + }, + { + UserSpec: api.UserSpec{ + Kind: api.ServiceAccountOwner, + Name: "system:serviceaccount:capsule-system:capsule", + }, + ClusterRoles: []string{"admin", "capsule-namespace-deleter"}, + }, + { + UserSpec: api.UserSpec{ + Kind: api.UserOwner, + Name: "e2e-owners-1", + }, + ClusterRoles: []string{"admin", "capsule-namespace-deleter"}, + }, + } + + Expect(normalizeOwners(t.Status.Owners)). + To(Equal(normalizeOwners(expectedOwners))) + + VerifyTenantRoleBindings(t) + }) + + By("checking owners (e2e-owners-2)", func() { + t := &capsulev1beta2.Tenant{} + Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: tnt2.GetName()}, t)).Should(Succeed()) + + expectedOwners := api.OwnerStatusListSpec{ + { + UserSpec: api.UserSpec{ + Kind: api.GroupOwner, + Name: "e2e-owners-2-group", + }, + ClusterRoles: []string{"admin", "capsule-namespace-deleter"}, + }, + { + UserSpec: api.UserSpec{ + Kind: api.ServiceAccountOwner, + Name: "system:serviceaccount:capsule-system:capsule", + }, + ClusterRoles: []string{"admin", "capsule-namespace-deleter"}, + }, + { + UserSpec: api.UserSpec{ + Kind: api.UserOwner, + Name: "e2e-owners-2", + }, + ClusterRoles: []string{"admin", "capsule-namespace-deleter"}, + }, + } + + Expect(normalizeOwners(t.Status.Owners)). + To(Equal(normalizeOwners(expectedOwners))) + + VerifyTenantRoleBindings(t) + }) + }) +}) diff --git a/e2e/pod_metadata_test.go b/e2e/pod_metadata_test.go index 1da7e2b2..50146470 100644 --- a/e2e/pod_metadata_test.go +++ b/e2e/pod_metadata_test.go @@ -24,9 +24,11 @@ var _ = Describe("adding metadata to Pod objects", Label("pod"), func() { Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "gatsby", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "gatsby", + Kind: "User", + }, }, }, }, diff --git a/e2e/pod_priority_class_test.go b/e2e/pod_priority_class_test.go index bd9e22f2..34e15cbf 100644 --- a/e2e/pod_priority_class_test.go +++ b/e2e/pod_priority_class_test.go @@ -30,9 +30,11 @@ var _ = Describe("enforcing a Priority Class", Label("pod", "classes"), func() { Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "paul", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "paul", + Kind: "User", + }, }, }, }, @@ -56,9 +58,11 @@ var _ = Describe("enforcing a Priority Class", Label("pod", "classes"), func() { Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "george", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "george", + Kind: "User", + }, }, }, }, @@ -85,9 +89,11 @@ var _ = Describe("enforcing a Priority Class", Label("pod", "classes"), func() { Spec: capsulev1beta2.TenantSpec{ Owners: []api.OwnerSpec{ { - UserSpec: api.UserSpec{ - Name: "e2e-priority-class-no-restrictions", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "e2e-priority-class-no-restrictions", + Kind: "User", + }, }, }, }, diff --git a/e2e/pod_runtime_class_test.go b/e2e/pod_runtime_class_test.go index cc751be1..84c1bada 100644 --- a/e2e/pod_runtime_class_test.go +++ b/e2e/pod_runtime_class_test.go @@ -31,9 +31,11 @@ var _ = Describe("enforcing a Runtime Class", Label("pod", "classes", "current") Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "george", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "george", + Kind: "User", + }, }, }, }, @@ -61,9 +63,11 @@ var _ = Describe("enforcing a Runtime Class", Label("pod", "classes", "current") Spec: capsulev1beta2.TenantSpec{ Owners: []api.OwnerSpec{ { - UserSpec: api.UserSpec{ - Name: "e2e-gateway-no-restrictions", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "e2e-gateway-no-restrictions", + Kind: "User", + }, }, }, }, diff --git a/e2e/preventing_pv_cross_tenant_mount_test.go b/e2e/preventing_pv_cross_tenant_mount_test.go index d374a8f8..d9f2772e 100644 --- a/e2e/preventing_pv_cross_tenant_mount_test.go +++ b/e2e/preventing_pv_cross_tenant_mount_test.go @@ -27,9 +27,11 @@ var _ = Describe("preventing PersistentVolume cross-tenant mount", Label("tenant Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "jessica", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "jessica", + Kind: "User", + }, }, }, }, @@ -43,9 +45,11 @@ var _ = Describe("preventing PersistentVolume cross-tenant mount", Label("tenant Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "leto", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "leto", + Kind: "User", + }, }, }, }, diff --git a/e2e/protected_namespace_regex_test.go b/e2e/protected_namespace_regex_test.go index e72bfa43..ee6482fe 100644 --- a/e2e/protected_namespace_regex_test.go +++ b/e2e/protected_namespace_regex_test.go @@ -25,9 +25,11 @@ var _ = Describe("creating a Namespace with a protected Namespace regex enabled" Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "alice", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "alice", + Kind: "User", + }, }, }, }, diff --git a/e2e/resource_quota_exceeded_test.go b/e2e/resource_quota_exceeded_test.go index 88aad1fb..b5017c9a 100644 --- a/e2e/resource_quota_exceeded_test.go +++ b/e2e/resource_quota_exceeded_test.go @@ -29,9 +29,11 @@ var _ = Describe("exceeding a Tenant resource quota", Label("resourcequota"), fu Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "bobby", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "bobby", + Kind: "User", + }, }, }, }, diff --git a/e2e/resourcepool_test.go b/e2e/resourcepool_test.go index 6d6208b6..a30e2816 100644 --- a/e2e/resourcepool_test.go +++ b/e2e/resourcepool_test.go @@ -17,8 +17,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" - "github.com/projectcapsule/capsule/pkg/api" "github.com/projectcapsule/capsule/pkg/api/meta" + "github.com/projectcapsule/capsule/pkg/api/misc" "github.com/projectcapsule/capsule/pkg/utils" ) @@ -83,7 +83,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() { }, }, Spec: capsulev1beta2.ResourcePoolSpec{ - Selectors: []api.NamespaceSelector{ + Selectors: []misc.NamespaceSelector{ { LabelSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ @@ -522,7 +522,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() { }, }, Spec: capsulev1beta2.ResourcePoolSpec{ - Selectors: []api.NamespaceSelector{ + Selectors: []misc.NamespaceSelector{ { LabelSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ @@ -694,7 +694,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() { }, }, Spec: capsulev1beta2.ResourcePoolSpec{ - Selectors: []api.NamespaceSelector{ + Selectors: []misc.NamespaceSelector{ { LabelSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ @@ -950,7 +950,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() { }, }, Spec: capsulev1beta2.ResourcePoolSpec{ - Selectors: []api.NamespaceSelector{ + Selectors: []misc.NamespaceSelector{ { LabelSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ @@ -1292,7 +1292,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() { }, }, Spec: capsulev1beta2.ResourcePoolSpec{ - Selectors: []api.NamespaceSelector{ + Selectors: []misc.NamespaceSelector{ { LabelSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ @@ -1450,7 +1450,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() { }, }, Spec: capsulev1beta2.ResourcePoolSpec{ - Selectors: []api.NamespaceSelector{ + Selectors: []misc.NamespaceSelector{ { LabelSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ @@ -1558,7 +1558,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() { }, }, Spec: capsulev1beta2.ResourcePoolSpec{ - Selectors: []api.NamespaceSelector{ + Selectors: []misc.NamespaceSelector{ { LabelSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ @@ -1666,7 +1666,7 @@ var _ = Describe("ResourcePool Tests", Label("resourcepool"), func() { }, }, Spec: capsulev1beta2.ResourcePoolSpec{ - Selectors: []api.NamespaceSelector{ + Selectors: []misc.NamespaceSelector{ { LabelSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ diff --git a/e2e/resourcepoolclaim_test.go b/e2e/resourcepoolclaim_test.go index 2433211a..c885a534 100644 --- a/e2e/resourcepoolclaim_test.go +++ b/e2e/resourcepoolclaim_test.go @@ -17,6 +17,7 @@ import ( capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" "github.com/projectcapsule/capsule/pkg/api" "github.com/projectcapsule/capsule/pkg/api/meta" + "github.com/projectcapsule/capsule/pkg/api/misc" ) var _ = Describe("ResourcePoolClaim Tests", Label("resourcepool"), func() { @@ -30,9 +31,11 @@ var _ = Describe("ResourcePoolClaim Tests", Label("resourcepool"), func() { Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "wind-user", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "wind-user", + Kind: "User", + }, }, }, }, @@ -99,7 +102,7 @@ var _ = Describe("ResourcePoolClaim Tests", Label("resourcepool"), func() { }, }, Spec: capsulev1beta2.ResourcePoolSpec{ - Selectors: []api.NamespaceSelector{ + Selectors: []misc.NamespaceSelector{ { LabelSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ @@ -302,7 +305,7 @@ var _ = Describe("ResourcePoolClaim Tests", Label("resourcepool"), func() { Config: capsulev1beta2.ResourcePoolSpecConfiguration{ DeleteBoundResources: ptr.To(false), }, - Selectors: []api.NamespaceSelector{ + Selectors: []misc.NamespaceSelector{ { LabelSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ @@ -492,7 +495,7 @@ var _ = Describe("ResourcePoolClaim Tests", Label("resourcepool"), func() { Config: capsulev1beta2.ResourcePoolSpecConfiguration{ DeleteBoundResources: ptr.To(false), }, - Selectors: []api.NamespaceSelector{ + Selectors: []misc.NamespaceSelector{ { LabelSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ @@ -521,7 +524,7 @@ var _ = Describe("ResourcePoolClaim Tests", Label("resourcepool"), func() { Config: capsulev1beta2.ResourcePoolSpecConfiguration{ DeleteBoundResources: ptr.To(false), }, - Selectors: []api.NamespaceSelector{ + Selectors: []misc.NamespaceSelector{ { LabelSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ diff --git a/e2e/sa_owner_promotion_test.go b/e2e/sa_owner_promotion_test.go index f9fd8628..6cfbe637 100644 --- a/e2e/sa_owner_promotion_test.go +++ b/e2e/sa_owner_promotion_test.go @@ -18,7 +18,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" - ctrlrbac "github.com/projectcapsule/capsule/internal/controllers/rbac" "github.com/projectcapsule/capsule/pkg/api" "github.com/projectcapsule/capsule/pkg/api/meta" ) @@ -33,9 +32,11 @@ var _ = Describe("Promoting ServiceAccounts to Owners", Label("config"), Label(" Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "alice", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "alice", + Kind: "User", + }, }, }, }, @@ -280,7 +281,7 @@ var _ = Describe("Promoting ServiceAccounts to Owners", Label("config"), Label(" Eventually(func(g Gomega) []rbacv1.Subject { crb := &rbacv1.ClusterRoleBinding{} - err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ctrlrbac.ProvisionerRoleName}, crb) + err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: api.ProvisionerRoleName}, crb) g.Expect(err).NotTo(HaveOccurred()) return crb.Subjects @@ -329,7 +330,7 @@ var _ = Describe("Promoting ServiceAccounts to Owners", Label("config"), Label(" Eventually(func(g Gomega) []rbacv1.Subject { crb := &rbacv1.ClusterRoleBinding{} - err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: ctrlrbac.ProvisionerRoleName}, crb) + err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: api.ProvisionerRoleName}, crb) g.Expect(err).NotTo(HaveOccurred()) return crb.Subjects @@ -351,5 +352,4 @@ var _ = Describe("Promoting ServiceAccounts to Owners", Label("config"), Label(" Expect(saClient.Delete(context.TODO(), secondNs)).To(Not(Succeed())) }) - }) diff --git a/e2e/sa_prevent_privilege_escalation_test.go b/e2e/sa_prevent_privilege_escalation_test.go index 1206d13f..4ce733f7 100644 --- a/e2e/sa_prevent_privilege_escalation_test.go +++ b/e2e/sa_prevent_privilege_escalation_test.go @@ -29,9 +29,11 @@ var _ = Describe("trying to escalate from a Tenant Namespace ServiceAccount", La Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "mario", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "mario", + Kind: "User", + }, }, }, }, diff --git a/e2e/scalability_test.go b/e2e/scalability_test.go index 3ba902b3..e22291b0 100644 --- a/e2e/scalability_test.go +++ b/e2e/scalability_test.go @@ -26,9 +26,11 @@ var _ = Describe("verify scalability", Label("scalability"), func() { Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "gatsby", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "gatsby", + Kind: "User", + }, }, }, }, diff --git a/e2e/selecting_non_owned_tenant_test.go b/e2e/selecting_non_owned_tenant_test.go index fe8e72f5..19a80c19 100644 --- a/e2e/selecting_non_owned_tenant_test.go +++ b/e2e/selecting_non_owned_tenant_test.go @@ -25,9 +25,11 @@ var _ = Describe("creating a Namespace trying to select a third Tenant", Label(" Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "undefined", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "undefined", + Kind: "User", + }, }, }, }, diff --git a/e2e/selecting_tenant_fail_test.go b/e2e/selecting_tenant_fail_test.go index f8a2a003..e941f1c6 100644 --- a/e2e/selecting_tenant_fail_test.go +++ b/e2e/selecting_tenant_fail_test.go @@ -22,9 +22,11 @@ var _ = Describe("creating a Namespace without a Tenant selector when user owns Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "john", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "john", + Kind: "User", + }, }, }, }, @@ -37,9 +39,11 @@ var _ = Describe("creating a Namespace without a Tenant selector when user owns Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "john", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "john", + Kind: "User", + }, }, }, }, @@ -52,9 +56,11 @@ var _ = Describe("creating a Namespace without a Tenant selector when user owns Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "john", - Kind: "Group", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "john", + Kind: "Group", + }, }, }, }, @@ -67,9 +73,11 @@ var _ = Describe("creating a Namespace without a Tenant selector when user owns Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "john", - Kind: "Group", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "john", + Kind: "Group", + }, }, }, }, diff --git a/e2e/selecting_tenant_with_label_test.go b/e2e/selecting_tenant_with_label_test.go index 96bae7f6..8835bb13 100644 --- a/e2e/selecting_tenant_with_label_test.go +++ b/e2e/selecting_tenant_with_label_test.go @@ -26,9 +26,11 @@ var _ = Describe("creating a Namespace with Tenant selector when user owns multi Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "john", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "john", + Kind: "User", + }, }, }, }, @@ -41,9 +43,11 @@ var _ = Describe("creating a Namespace with Tenant selector when user owns multi Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "john", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "john", + Kind: "User", + }, }, }, }, diff --git a/e2e/service_forbidden_metadata_test.go b/e2e/service_forbidden_metadata_test.go index b6c4c7fd..a36c3a02 100644 --- a/e2e/service_forbidden_metadata_test.go +++ b/e2e/service_forbidden_metadata_test.go @@ -34,9 +34,11 @@ var _ = Describe("creating a Service with user-specified labels and annotations" }, Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "gatsby", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "gatsby", + Kind: "User", + }, }, }, }, diff --git a/e2e/service_metadata_test.go b/e2e/service_metadata_test.go index f1e58ff4..188090c9 100644 --- a/e2e/service_metadata_test.go +++ b/e2e/service_metadata_test.go @@ -31,9 +31,11 @@ var _ = Describe("adding metadata to Service objects", Label("tenant", "service" Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "gatsby", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "gatsby", + Kind: "User", + }, }, }, }, diff --git a/e2e/storage_class_test.go b/e2e/storage_class_test.go index 0ed64154..fff98890 100644 --- a/e2e/storage_class_test.go +++ b/e2e/storage_class_test.go @@ -35,9 +35,11 @@ var _ = Describe("when Tenant handles Storage classes", Label("tenant", "classes Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "selector", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "selector", + Kind: "User", + }, }, }, }, @@ -64,9 +66,11 @@ var _ = Describe("when Tenant handles Storage classes", Label("tenant", "classes Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "default", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "default", + Kind: "User", + }, }, }, }, @@ -90,9 +94,11 @@ var _ = Describe("when Tenant handles Storage classes", Label("tenant", "classes Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "no-restrictions", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "no-restrictions", + Kind: "User", + }, }, }, }, diff --git a/e2e/tenant_cordoning_test.go b/e2e/tenant_cordoning_test.go index 54c294f3..8da6bad2 100644 --- a/e2e/tenant_cordoning_test.go +++ b/e2e/tenant_cordoning_test.go @@ -27,9 +27,11 @@ var _ = Describe("cordoning a Tenant", Label("tenant"), func() { Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "jim", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "jim", + Kind: "User", + }, }, }, }, diff --git a/e2e/tenant_metadata_test.go b/e2e/tenant_metadata_test.go index 2c473fa0..0fc195d7 100644 --- a/e2e/tenant_metadata_test.go +++ b/e2e/tenant_metadata_test.go @@ -35,9 +35,11 @@ var _ = Describe("adding metadata to a Tenant", Label("tenant"), func() { Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "jim", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "jim", + Kind: "User", + }, }, }, }, diff --git a/e2e/tenant_name_webhook_test.go b/e2e/tenant_name_webhook_test.go index b1683d8e..c3ba101e 100644 --- a/e2e/tenant_name_webhook_test.go +++ b/e2e/tenant_name_webhook_test.go @@ -22,9 +22,11 @@ var _ = Describe("creating a Tenant with wrong name", Label("tenant"), func() { Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "john", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "john", + Kind: "User", + }, }, }, }, diff --git a/e2e/tenant_protected_webhook_test.go b/e2e/tenant_protected_webhook_test.go index 2790dae8..2c0d6a7e 100644 --- a/e2e/tenant_protected_webhook_test.go +++ b/e2e/tenant_protected_webhook_test.go @@ -24,9 +24,11 @@ var _ = Describe("Deleting a tenant with protected annotation", Label("tenant"), PreventDeletion: true, Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "john", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "john", + Kind: "User", + }, }, }, }, diff --git a/e2e/tenant_resources_changes_test.go b/e2e/tenant_resources_changes_test.go index 815e2894..eca3c8dd 100644 --- a/e2e/tenant_resources_changes_test.go +++ b/e2e/tenant_resources_changes_test.go @@ -29,9 +29,11 @@ var _ = Describe("changing Tenant managed Kubernetes resources", Label("tenant") Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "laura", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "laura", + Kind: "User", + }, }, }, }, diff --git a/e2e/tenant_resources_test.go b/e2e/tenant_resources_test.go index df451ef2..ed9a0393 100644 --- a/e2e/tenant_resources_test.go +++ b/e2e/tenant_resources_test.go @@ -29,9 +29,11 @@ var _ = Describe("creating namespaces within a Tenant with resources", Label("te Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "john", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "john", + Kind: "User", + }, }, }, }, diff --git a/e2e/tenantresource_test.go b/e2e/tenantresource_test.go index 3233b1b3..700b239f 100644 --- a/e2e/tenantresource_test.go +++ b/e2e/tenantresource_test.go @@ -32,9 +32,11 @@ var _ = Describe("Creating a TenantResource object", Label("tenantresource"), fu Spec: capsulev1beta2.TenantSpec{ Owners: api.OwnerListSpec{ { - UserSpec: api.UserSpec{ - Name: "solar-user", - Kind: "User", + CoreOwnerSpec: api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: "solar-user", + Kind: "User", + }, }, }, }, diff --git a/e2e/utils_test.go b/e2e/utils_test.go index bf2ef65d..66876dd8 100644 --- a/e2e/utils_test.go +++ b/e2e/utils_test.go @@ -8,6 +8,7 @@ import ( "encoding/json" "fmt" "reflect" + "sort" "strings" "time" @@ -327,6 +328,66 @@ func CheckForOwnerRoleBindings(ns *corev1.Namespace, owner api.OwnerSpec, roles } } +func VerifyTenantRoleBindings( + tnt *capsulev1beta2.Tenant, +) { + Eventually(func(g Gomega) { + // List all RoleBindings once per namespace to avoid repeated API calls. + for _, ns := range tnt.Status.Namespaces { + for i, owner := range tnt.Status.Owners { + for _, role := range owner.ClusterRoles { + rbName := fmt.Sprintf("capsule-%s-%d-%s", tnt.Name, i, role) + + rb := &rbacv1.RoleBinding{} + err := k8sClient.Get(context.Background(), client.ObjectKey{ + Namespace: ns, + Name: rbName, + }, rb) + + g.Expect(err).ToNot(HaveOccurred(), + "expected RoleBinding %s/%s to exist", ns, rbName) + + g.Expect(rb.RoleRef.Name).To(Equal(role), + "expected RoleBinding %s/%s to have RoleRef.Name=%q", + ns, rbName, role) + + g.Expect(rb.Subjects).ToNot(BeEmpty(), + "expected RoleBinding %s/%s to have at least one subject", ns, rbName) + + foundSubject := false + for _, s := range rb.Subjects { + if s.Kind == string(owner.Kind) && s.Name == owner.Name { + foundSubject = true + break + } + } + + g.Expect(foundSubject).To(BeTrue(), + "expected RoleBinding %s/%s to contain subject %s/%s", + ns, rb.Name, owner.Kind, owner.Name) + + } + } + } + }).WithTimeout(30 * time.Second).WithPolling(500 * time.Millisecond).Should(Succeed()) +} + +func normalizeOwners(in api.OwnerStatusListSpec) api.OwnerStatusListSpec { + // copy to avoid mutating the original + out := make(api.OwnerStatusListSpec, len(in)) + copy(out, in) + + // sort outer slice by kind+name + sort.Sort(api.GetByKindAndName(out)) + + // sort roles inside each owner so role order doesn't matter + for i := range out { + sort.Strings(out[i].ClusterRoles) + } + + return out +} + func GetKubernetesVersion() *versionUtil.Version { var serverVersion *version.Info var err error diff --git a/go.sum b/go.sum index 638ddbb9..6e61dee2 100644 --- a/go.sum +++ b/go.sum @@ -232,8 +232,6 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= diff --git a/internal/controllers/rbac/manager.go b/internal/controllers/rbac/manager.go index dc330112..83ec53d0 100644 --- a/internal/controllers/rbac/manager.go +++ b/internal/controllers/rbac/manager.go @@ -38,7 +38,7 @@ type Manager struct { //nolint:revive func (r *Manager) SetupWithManager(ctx context.Context, mgr ctrl.Manager, ctrlConfig utils.ControllerOptions) (err error) { - namesPredicate := utils.NamesMatchingPredicate(ProvisionerRoleName, DeleterRoleName) + namesPredicate := utils.NamesMatchingPredicate(api.ProvisionerRoleName, api.DeleterRoleName) crErr := ctrl.NewControllerManagedBy(mgr). For(&rbacv1.ClusterRole{}, namesPredicate). @@ -63,7 +63,7 @@ func (r *Manager) SetupWithManager(ctx context.Context, mgr ctrl.Manager, ctrlCo r.handleSAChange(ctx, e.Object) }, UpdateFunc: func(ctx context.Context, e event.TypedUpdateEvent[client.Object], q workqueue.TypedRateLimitingInterface[reconcile.Request]) { - if promotionLabelsChanged(e.ObjectOld.GetLabels(), e.ObjectNew.GetLabels()) { + if utils.LabelsChanged([]string{meta.OwnerPromotionLabel}, e.ObjectOld.GetLabels(), e.ObjectNew.GetLabels()) { r.handleSAChange(ctx, e.ObjectNew) } }, @@ -83,9 +83,9 @@ func (r *Manager) SetupWithManager(ctx context.Context, mgr ctrl.Manager, ctrlCo // Resource kinds and we're just interested to the ones with the said name since they're bounded together. func (r *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res reconcile.Result, err error) { switch request.Name { - case ProvisionerRoleName: - if err = r.EnsureClusterRole(ctx, ProvisionerRoleName); err != nil { - r.Log.Error(err, "Reconciliation for ClusterRole failed", "ClusterRole", ProvisionerRoleName) + case api.ProvisionerRoleName: + if err = r.EnsureClusterRole(ctx, api.ProvisionerRoleName); err != nil { + r.Log.Error(err, "Reconciliation for ClusterRole failed", "ClusterRole", api.ProvisionerRoleName) break } @@ -95,9 +95,9 @@ func (r *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res break } - case DeleterRoleName: - if err = r.EnsureClusterRole(ctx, DeleterRoleName); err != nil { - r.Log.Error(err, "Reconciliation for ClusterRole failed", "ClusterRole", DeleterRoleName) + case api.DeleterRoleName: + if err = r.EnsureClusterRole(ctx, api.DeleterRoleName); err != nil { + r.Log.Error(err, "Reconciliation for ClusterRole failed", "ClusterRole", api.DeleterRoleName) } } @@ -106,12 +106,12 @@ func (r *Manager) Reconcile(ctx context.Context, request reconcile.Request) (res func (r *Manager) EnsureClusterRoleBindingsProvisioner(ctx context.Context) error { crb := &rbacv1.ClusterRoleBinding{ - ObjectMeta: metav1.ObjectMeta{Name: ProvisionerRoleName}, + ObjectMeta: metav1.ObjectMeta{Name: api.ProvisionerRoleName}, } return retry.RetryOnConflict(retry.DefaultRetry, func() error { _, err := controllerutil.CreateOrUpdate(ctx, r.Client, crb, func() error { - crb.RoleRef = provisionerClusterRoleBinding.RoleRef + crb.RoleRef = api.ProvisionerClusterRoleBinding.RoleRef crb.Subjects = nil for _, entity := range r.Configuration.Administrators() { @@ -179,7 +179,7 @@ func (r *Manager) EnsureClusterRoleBindingsProvisioner(ctx context.Context) erro } func (r *Manager) EnsureClusterRole(ctx context.Context, roleName string) (err error) { - role, ok := clusterRoles[roleName] + role, ok := api.ClusterRoles[roleName] if !ok { return fmt.Errorf("clusterRole %s is not mapped", roleName) } @@ -203,7 +203,7 @@ func (r *Manager) EnsureClusterRole(ctx context.Context, roleName string) (err e // since we're not creating empty CR and CRB upon Capsule installation: it's a run-once task, since the reconciliation // is handled by the Reconciler implemented interface. func (r *Manager) Start(ctx context.Context) error { - for roleName := range clusterRoles { + for roleName := range api.ClusterRoles { r.Log.V(4).Info("setting up ClusterRoles", "ClusterRole", roleName) if err := r.EnsureClusterRole(ctx, roleName); err != nil { @@ -237,20 +237,3 @@ func (r *Manager) handleSAChange(ctx context.Context, obj client.Object) { r.Log.Error(err, "cannot update ClusterRoleBinding upon ServiceAccount event") } } - -func promotionLabelsChanged(oldLabels, newLabels map[string]string) bool { - keys := []string{ - meta.OwnerPromotionLabel, - } - - for _, key := range keys { - oldVal, oldOK := oldLabels[key] - newVal, newOK := newLabels[key] - - if oldOK != newOK || oldVal != newVal { - return true - } - } - - return false -} diff --git a/internal/controllers/tenant/manager.go b/internal/controllers/tenant/manager.go index f82ef895..4285c967 100644 --- a/internal/controllers/tenant/manager.go +++ b/internal/controllers/tenant/manager.go @@ -17,20 +17,25 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/apiserver/pkg/authentication/serviceaccount" "k8s.io/client-go/rest" "k8s.io/client-go/tools/record" "k8s.io/client-go/util/retry" + "k8s.io/client-go/util/workqueue" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" "github.com/projectcapsule/capsule/internal/controllers/utils" "github.com/projectcapsule/capsule/internal/metrics" + "github.com/projectcapsule/capsule/pkg/api" meta "github.com/projectcapsule/capsule/pkg/api/meta" "github.com/projectcapsule/capsule/pkg/configuration" ) @@ -47,7 +52,12 @@ type Manager struct { func (r *Manager) SetupWithManager(mgr ctrl.Manager, ctrlConfig utils.ControllerOptions) error { return ctrl.NewControllerManagedBy(mgr). - For(&capsulev1beta2.Tenant{}). + For( + &capsulev1beta2.Tenant{}, + builder.WithPredicates( + predicate.GenerationChangedPredicate{}, + ), + ). Owns(&networkingv1.NetworkPolicy{}). Owns(&corev1.LimitRange{}). Owns(&corev1.ResourceQuota{}). @@ -64,29 +74,129 @@ func (r *Manager) SetupWithManager(mgr ctrl.Manager, ctrlConfig utils.Controller ). Watches( &storagev1.StorageClass{}, - handler.EnqueueRequestsFromMapFunc(r.enqueueAllTenants), + r.statusOnlyHandlerClasses( + r.reconcileClassStatus, + r.collectAvailableStorageClasses, + "cannot collect storage classes", + ), builder.WithPredicates(utils.UpdatedMetadataPredicate), ). Watches( &gatewayv1.GatewayClass{}, - handler.EnqueueRequestsFromMapFunc(r.enqueueAllTenants), - builder.WithPredicates(utils.UpdatedMetadataPredicate), - ). - Watches( - &networkingv1.IngressClass{}, - handler.EnqueueRequestsFromMapFunc(r.enqueueAllTenants), + r.statusOnlyHandlerClasses( + r.reconcileClassStatus, + r.collectAvailableGatewayClasses, + "cannot collect gateway classes", + ), builder.WithPredicates(utils.UpdatedMetadataPredicate), ). Watches( &schedulingv1.PriorityClass{}, - handler.EnqueueRequestsFromMapFunc(r.enqueueAllTenants), + r.statusOnlyHandlerClasses( + r.reconcileClassStatus, + r.collectAvailablePriorityClasses, + "cannot collect priority classes", + ), builder.WithPredicates(utils.UpdatedMetadataPredicate), ). Watches( &nodev1.RuntimeClass{}, - handler.EnqueueRequestsFromMapFunc(r.enqueueAllTenants), + r.statusOnlyHandlerClasses( + r.reconcileClassStatus, + r.collectAvailableRuntimeClasses, + "cannot collect runtime classes", + ), builder.WithPredicates(utils.UpdatedMetadataPredicate), ). + Watches( + &capsulev1beta2.TenantOwner{}, + handler.TypedFuncs[client.Object, ctrl.Request]{ + CreateFunc: func( + ctx context.Context, + e event.TypedCreateEvent[client.Object], + q workqueue.TypedRateLimitingInterface[reconcile.Request], + ) { + r.enqueueForTenantsWithCondition( + ctx, + e.Object, + q, + func(tnt *capsulev1beta2.Tenant, c client.Object) bool { + return len(tnt.Spec.Permissions.MatchOwners) > 0 + }) + }, + UpdateFunc: func( + ctx context.Context, + e event.TypedUpdateEvent[client.Object], + q workqueue.TypedRateLimitingInterface[reconcile.Request], + ) { + r.enqueueForTenantsWithCondition( + ctx, + e.ObjectNew, + q, + func(tnt *capsulev1beta2.Tenant, c client.Object) bool { + return len(tnt.Spec.Permissions.MatchOwners) > 0 + }) + }, + + DeleteFunc: func( + ctx context.Context, + e event.TypedDeleteEvent[client.Object], + q workqueue.TypedRateLimitingInterface[reconcile.Request], + ) { + r.enqueueTenantsForTenantOwner(ctx, e.Object, q) + }, + }, + ). + Watches( + &corev1.ServiceAccount{}, + handler.TypedFuncs[client.Object, ctrl.Request]{ + CreateFunc: func( + ctx context.Context, + e event.TypedCreateEvent[client.Object], + q workqueue.TypedRateLimitingInterface[reconcile.Request], + ) { + r.enqueueForTenantsWithCondition(ctx, e.Object, q, func(tnt *capsulev1beta2.Tenant, c client.Object) bool { + for _, n := range tnt.Status.Namespaces { + if n == c.GetNamespace() { + return true + } + } + + return false + }) + }, + UpdateFunc: func( + ctx context.Context, + e event.TypedUpdateEvent[client.Object], + q workqueue.TypedRateLimitingInterface[reconcile.Request], + ) { + r.enqueueForTenantsWithCondition(ctx, e.ObjectNew, q, func(tnt *capsulev1beta2.Tenant, c client.Object) bool { + for _, n := range tnt.Status.Namespaces { + if n == c.GetNamespace() { + return true + } + } + + return false + }) + }, + DeleteFunc: func( + ctx context.Context, + e event.TypedDeleteEvent[client.Object], + q workqueue.TypedRateLimitingInterface[reconcile.Request], + ) { + r.enqueueForTenantsWithCondition(ctx, e.Object, q, func(tnt *capsulev1beta2.Tenant, c client.Object) bool { + _, found := tnt.Status.Owners.FindOwner( + serviceaccount.ServiceAccountUsernamePrefix+c.GetNamespace()+":"+c.GetName(), + api.ServiceAccountOwner, + ) + + return found + }) + }, + }, + builder.WithPredicates(utils.PromotedServiceaccountPredicate), + ). WithOptions(controller.Options{MaxConcurrentReconciles: ctrlConfig.MaxConcurrentReconciles}). Complete(r) } @@ -120,6 +230,13 @@ func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ct } }() + // Collect Ownership for Status + if err = r.collectOwners(ctx, instance); err != nil { + err = fmt.Errorf("cannot collect available owners: %w", err) + + return result, err + } + // Ensuring Metadata. err, updated := r.ensureMetadata(ctx, instance) if err != nil { diff --git a/internal/controllers/tenant/rolebindings.go b/internal/controllers/tenant/rolebindings.go index a18a4b5b..5add5878 100644 --- a/internal/controllers/tenant/rolebindings.go +++ b/internal/controllers/tenant/rolebindings.go @@ -15,28 +15,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" - "github.com/projectcapsule/capsule/internal/controllers/rbac" "github.com/projectcapsule/capsule/pkg/api" "github.com/projectcapsule/capsule/pkg/api/meta" ) -// ownerClusterRoleBindings generates a Capsule AdditionalRoleBinding object for the Owner dynamic clusterrole in order -// to take advantage of the additional role binding feature. -func (r *Manager) ownerClusterRoleBindings(owner api.OwnerSpec, clusterRole string) api.AdditionalRoleBindingsSpec { - rb := r.userClusterRoleBindings(owner.UserSpec, clusterRole) - - if owner.Labels != nil { - rb.Labels = owner.Labels - } - - if owner.Annotations != nil { - rb.Labels = owner.Annotations - } - - return rb -} - -func (r *Manager) userClusterRoleBindings(owner api.UserSpec, clusterRole string) api.AdditionalRoleBindingsSpec { +func (r *Manager) userClusterRoleBindings(owner api.CoreOwnerSpec, clusterRole string) api.AdditionalRoleBindingsSpec { var subject rbacv1.Subject if owner.Kind == "ServiceAccount" { @@ -78,12 +61,13 @@ func (r *Manager) syncRoleBindings(ctx context.Context, tenant *capsulev1beta2.T return fmt.Sprintf("%x", h.Sum64()) } + // getting requested Role Binding keys - keys := make([]string, 0, len(tenant.Spec.Owners)) + keys := make([]string, 0, len(tenant.Status.Owners)) // Generating for dynamic tenant owners cluster roles - for _, owner := range tenant.Spec.Owners { + for _, owner := range tenant.Status.Owners { for _, clusterRoleName := range owner.ClusterRoles { - cr := r.ownerClusterRoleBindings(owner, clusterRoleName) + cr := r.userClusterRoleBindings(owner, clusterRoleName) keys = append(keys, hashFn(cr)) } @@ -93,12 +77,6 @@ func (r *Manager) syncRoleBindings(ctx context.Context, tenant *capsulev1beta2.T keys = append(keys, hashFn(i)) } - for _, i := range r.Configuration.Administrators() { - cr := r.userClusterRoleBindings(i, rbac.DeleterRoleName) - - keys = append(keys, hashFn(cr)) - } - group := new(errgroup.Group) for _, ns := range tenant.Status.Namespaces { @@ -119,16 +97,12 @@ func (r *Manager) syncAdditionalRoleBinding(ctx context.Context, tenant *capsule roleBindings := make([]api.AdditionalRoleBindingsSpec, 0) - for _, owner := range tenant.Spec.Owners { + for _, owner := range tenant.Status.Owners { for _, clusterRoleName := range owner.ClusterRoles { - roleBindings = append(roleBindings, r.ownerClusterRoleBindings(owner, clusterRoleName)) + roleBindings = append(roleBindings, r.userClusterRoleBindings(owner, clusterRoleName)) } } - for _, a := range r.Configuration.Administrators() { - roleBindings = append(roleBindings, r.userClusterRoleBindings(a, rbac.DeleterRoleName)) - } - roleBindings = append(roleBindings, tenant.Spec.AdditionalRoleBindings...) for i, roleBinding := range roleBindings { diff --git a/internal/controllers/tenant/status.go b/internal/controllers/tenant/status.go index 069ef2e0..ac595a65 100644 --- a/internal/controllers/tenant/status.go +++ b/internal/controllers/tenant/status.go @@ -5,6 +5,7 @@ package tenant import ( "context" + "fmt" "sort" nodev1 "k8s.io/api/node/v1" @@ -21,11 +22,86 @@ import ( "github.com/projectcapsule/capsule/pkg/api" ) +// Sets a label on the Tenant object with it's name. +func (r *Manager) collectOwners(ctx context.Context, tnt *capsulev1beta2.Tenant) (err error) { + owners, err := tnt.CollectOwners( + ctx, + r.Client, + r.Configuration.AllowServiceAccountPromotion(), + r.Configuration.Administrators(), + ) + if err != nil { + return err + } + + // No Direct Update needed as status is always posted + tnt.Status.Owners = owners + + return nil +} + +func (r Manager) reconcileClassStatus( + ctx context.Context, + fn func(context.Context, *capsulev1beta2.Tenant) error, +) (err error) { + tntList := &capsulev1beta2.TenantList{} + if err = r.List(ctx, tntList); err != nil { + return err + } + + for i := range tntList.Items { + t := &tntList.Items[i] + + // Collect Ownership for Status + if err = fn(ctx, t); err != nil { + err = fmt.Errorf("cannot collect available classes: %w", err) + + return err + } + + if err = r.updateTenantStatus(ctx, t, err); err != nil { + err = fmt.Errorf("cannot update tenant status: %w", err) + + return err + } + } + + return err +} + func (r *Manager) collectAvailableResources(ctx context.Context, tnt *capsulev1beta2.Tenant) (err error) { log := log.FromContext(ctx) log.V(5).Info("collecting available storageclasses") + if err = r.collectAvailableStorageClasses(ctx, tnt); err != nil { + return err + } + + log.V(5).Info("collected available storageclasses", "size", len(tnt.Status.Classes.StorageClasses)) + + if err = r.collectAvailablePriorityClasses(ctx, tnt); err != nil { + return err + } + + log.V(5).Info("collected available priorityclasses", "size", len(tnt.Status.Classes.PriorityClasses)) + + if err = r.collectAvailableGatewayClasses(ctx, tnt); err != nil { + return err + } + + log.V(5).Info("collected available gatewayclasses", "size", len(tnt.Status.Classes.GatewayClasses)) + + if err = r.collectAvailableRuntimeClasses(ctx, tnt); err != nil { + return err + } + + log.V(5).Info("collected available runtimeclasses", "size", len(tnt.Status.Classes.RuntimeClasses)) + + return nil +} + +func (r *Manager) collectAvailableStorageClasses(ctx context.Context, tnt *capsulev1beta2.Tenant) (err error) { if tnt.Status.Classes.StorageClasses, err = listObjectNamesBySelector( ctx, r.Client, @@ -35,8 +111,10 @@ func (r *Manager) collectAvailableResources(ctx context.Context, tnt *capsulev1b return err } - log.V(5).Info("collected available storageclasses", "size", len(tnt.Status.Classes.StorageClasses)) + return nil +} +func (r *Manager) collectAvailablePriorityClasses(ctx context.Context, tnt *capsulev1beta2.Tenant) (err error) { if tnt.Status.Classes.PriorityClasses, err = listObjectNamesBySelector( ctx, r.Client, @@ -46,8 +124,10 @@ func (r *Manager) collectAvailableResources(ctx context.Context, tnt *capsulev1b return err } - log.V(5).Info("collected available priorityclasses", "size", len(tnt.Status.Classes.PriorityClasses)) + return nil +} +func (r *Manager) collectAvailableGatewayClasses(ctx context.Context, tnt *capsulev1beta2.Tenant) (err error) { if tnt.Status.Classes.GatewayClasses, err = listObjectNamesBySelector( ctx, r.Client, @@ -57,8 +137,10 @@ func (r *Manager) collectAvailableResources(ctx context.Context, tnt *capsulev1b return err } - log.V(5).Info("collected available gatewayclasses", "size", len(tnt.Status.Classes.GatewayClasses)) + return nil +} +func (r *Manager) collectAvailableRuntimeClasses(ctx context.Context, tnt *capsulev1beta2.Tenant) (err error) { if tnt.Status.Classes.RuntimeClasses, err = listObjectNamesBySelector( ctx, r.Client, @@ -68,8 +150,6 @@ func (r *Manager) collectAvailableResources(ctx context.Context, tnt *capsulev1b return err } - log.V(5).Info("collected available runtimeclasses", "size", len(tnt.Status.Classes.RuntimeClasses)) - return nil } @@ -81,13 +161,7 @@ func listObjectNamesBySelector( allowed *api.DefaultAllowedListSpec, list client.ObjectList, opts ...client.ListOption, -) (objects []string, err error) { - defer func() { - if err == nil { - sort.Strings(objects) - } - }() - +) ([]string, error) { if err := c.List(ctx, list, opts...); err != nil { return nil, err } @@ -97,8 +171,9 @@ func listObjectNamesBySelector( return nil, err } - allNames := make(map[string]struct{}) + objects := make([]string, 0) + allNames := make(map[string]struct{}) selected := make(map[string]struct{}) hasSelector := false @@ -117,9 +192,12 @@ func listObjectNamesBySelector( objects = append(objects, accessor.GetName()) } + sort.Strings(objects) + return objects, nil } + // Prepare selector var sel labels.Selector if hasSelector { sel, err = metav1.LabelSelectorAsSelector(&allowed.LabelSelector) @@ -128,6 +206,7 @@ func listObjectNamesBySelector( } } + // Evaluate objects for _, obj := range objs { accessor, err := meta.Accessor(obj) if err != nil { @@ -140,7 +219,6 @@ func listObjectNamesBySelector( if hasSelector { lbls := labels.Set(accessor.GetLabels()) - if sel.Matches(lbls) { selected[name] = struct{}{} } @@ -157,10 +235,6 @@ func listObjectNamesBySelector( continue } - if _, already := selected[name]; already { - continue - } - selected[name] = struct{}{} } @@ -168,5 +242,7 @@ func listObjectNamesBySelector( objects = append(objects, name) } + sort.Strings(objects) + return objects, nil } diff --git a/internal/controllers/tenant/utils.go b/internal/controllers/tenant/utils.go index a8c54ce8..d9fdd5e6 100644 --- a/internal/controllers/tenant/utils.go +++ b/internal/controllers/tenant/utils.go @@ -12,14 +12,116 @@ import ( "k8s.io/apimachinery/pkg/selection" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/retry" + "k8s.io/client-go/util/workqueue" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/reconcile" capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" "github.com/projectcapsule/capsule/pkg/utils" ) +func (r *Manager) statusOnlyHandlerClasses( + fn func(ctx context.Context, perTenant func(context.Context, *capsulev1beta2.Tenant) error) error, + perTenant func(context.Context, *capsulev1beta2.Tenant) error, + errMsg string, +) *handler.TypedFuncs[client.Object, reconcile.Request] { + return &handler.TypedFuncs[client.Object, reconcile.Request]{ + CreateFunc: func( + ctx context.Context, + _ event.TypedCreateEvent[client.Object], + _ workqueue.TypedRateLimitingInterface[reconcile.Request], + ) { + if err := fn(ctx, perTenant); err != nil { + r.Log.Error(err, errMsg) + } + }, + UpdateFunc: func( + ctx context.Context, + _ event.TypedUpdateEvent[client.Object], + _ workqueue.TypedRateLimitingInterface[reconcile.Request], + ) { + if err := fn(ctx, perTenant); err != nil { + r.Log.Error(err, errMsg) + } + }, + DeleteFunc: func( + ctx context.Context, + _ event.TypedDeleteEvent[client.Object], + _ workqueue.TypedRateLimitingInterface[reconcile.Request], + ) { + if err := fn(ctx, perTenant); err != nil { + r.Log.Error(err, errMsg) + } + }, + } +} + +func (r *Manager) enqueueTenantsForTenantOwner( + ctx context.Context, + tenantOwner client.Object, + q workqueue.TypedRateLimitingInterface[reconcile.Request], +) { + var tenants capsulev1beta2.TenantList + if err := r.List(ctx, &tenants); err != nil { + r.Log.Error(err, "failed to list Tenants for Tenant Owner event") + + return + } + + owner, ok := tenantOwner.(*capsulev1beta2.TenantOwner) + if !ok { + return + } + + for i := range tenants.Items { + tnt := &tenants.Items[i] + + if _, found := tnt.Status.Owners.FindOwner( + owner.Spec.Name, + owner.Spec.Kind, + ); !found { + continue + } + + q.Add(reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: tnt.Name, + }, + }) + } +} + +func (r *Manager) enqueueForTenantsWithCondition( + ctx context.Context, + obj client.Object, + q workqueue.TypedRateLimitingInterface[reconcile.Request], + fn func(*capsulev1beta2.Tenant, client.Object) bool, +) { + var tenants capsulev1beta2.TenantList + if err := r.List(ctx, &tenants); err != nil { + r.Log.Error(err, "failed to list Tenants for class event") + + return + } + + for i := range tenants.Items { + tnt := &tenants.Items[i] + + if !fn(tnt, obj) { + continue + } + + q.Add(reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: tnt.Name, + }, + }) + } +} + func (r *Manager) enqueueAllTenants(ctx context.Context, _ client.Object) []reconcile.Request { var tenants capsulev1beta2.TenantList if err := r.List(ctx, &tenants); err != nil { diff --git a/internal/controllers/utils/predicates.go b/internal/controllers/utils/predicates.go index 268d18de..df70310a 100644 --- a/internal/controllers/utils/predicates.go +++ b/internal/controllers/utils/predicates.go @@ -10,6 +10,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" + "github.com/projectcapsule/capsule/pkg/api/meta" ) var CapsuleConfigSpecChangedPredicate = predicate.Funcs{ @@ -32,6 +33,31 @@ var CapsuleConfigSpecChangedPredicate = predicate.Funcs{ GenericFunc: func(e event.GenericEvent) bool { return false }, } +var PromotedServiceaccountPredicate = predicate.TypedFuncs[client.Object]{ + CreateFunc: func(e event.TypedCreateEvent[client.Object]) bool { + v, ok := e.Object.GetLabels()[meta.OwnerPromotionLabel] + + return ok && v == meta.OwnerPromotionLabelTrigger + }, + + DeleteFunc: func(e event.TypedDeleteEvent[client.Object]) bool { + v, ok := e.Object.GetLabels()[meta.OwnerPromotionLabel] + + return ok && v == meta.OwnerPromotionLabelTrigger + }, + + UpdateFunc: func(e event.TypedUpdateEvent[client.Object]) bool { + oldVal, oldOK := e.ObjectOld.GetLabels()[meta.OwnerPromotionLabel] + newVal, newOK := e.ObjectNew.GetLabels()[meta.OwnerPromotionLabel] + + return oldOK != newOK || oldVal != newVal + }, + + GenericFunc: func(event.TypedGenericEvent[client.Object]) bool { + return false + }, +} + var UpdatedMetadataPredicate = predicate.Funcs{ CreateFunc: func(e event.CreateEvent) bool { return true }, DeleteFunc: func(e event.DeleteEvent) bool { return true }, @@ -57,6 +83,19 @@ func labelsEqual(a, b map[string]string) bool { return true } +func LabelsChanged(keys []string, oldLabels, newLabels map[string]string) bool { + for _, key := range keys { + oldVal, oldOK := oldLabels[key] + newVal, newOK := newLabels[key] + + if oldOK != newOK || oldVal != newVal { + return true + } + } + + return false +} + func NamesMatchingPredicate(names ...string) builder.Predicates { return builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool { for _, name := range names { diff --git a/internal/webhook/namespace/mutation/handler.go b/internal/webhook/namespace/mutation/handler.go index e0039ee8..42ac7776 100644 --- a/internal/webhook/namespace/mutation/handler.go +++ b/internal/webhook/namespace/mutation/handler.go @@ -132,12 +132,7 @@ func (h *handler) OnUpdate(c client.Client, decoder admission.Decoder, recorder } } } else { - owned, err := tenant.NamespaceIsOwned(ctx, c, h.cfg, oldNs, tnt, req.UserInfo) - if err != nil { - return utils.ErroredResponse(err) - } - - if !owned { + if owned := tenant.NamespaceIsOwned(ctx, c, h.cfg, oldNs, tnt, req.UserInfo); !owned { recorder.Eventf(oldNs, corev1.EventTypeWarning, "NamespacePatch", "Namespace %s can not be patched", oldNs.GetName()) response := admission.Denied("Denied patch request for this namespace") diff --git a/internal/webhook/namespace/validation/patch.go b/internal/webhook/namespace/validation/patch.go index 4bfeb192..7878e2c6 100644 --- a/internal/webhook/namespace/validation/patch.go +++ b/internal/webhook/namespace/validation/patch.go @@ -6,7 +6,6 @@ package validation import ( "context" "fmt" - "net/http" corev1 "k8s.io/api/core/v1" "k8s.io/client-go/tools/record" @@ -62,14 +61,7 @@ func (h *patchHandler) OnUpdate( return func(ctx context.Context, req admission.Request) *admission.Response { e := fmt.Sprintf("namespace/%s can not be patched", ns.Name) - ok, err := users.IsTenantOwner(ctx, c, h.cfg, tnt, req.UserInfo) - if err != nil { - response := admission.Errored(http.StatusBadRequest, err) - - return &response - } - - if ok { + if ok := users.IsTenantOwnerByStatus(ctx, c, h.cfg, tnt, req.UserInfo); ok { return nil } diff --git a/internal/webhook/serviceaccounts/validating.go b/internal/webhook/serviceaccounts/validating.go index a34a573e..cd96b928 100644 --- a/internal/webhook/serviceaccounts/validating.go +++ b/internal/webhook/serviceaccounts/validating.go @@ -13,7 +13,6 @@ import ( capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" capsulewebhook "github.com/projectcapsule/capsule/internal/webhook" - "github.com/projectcapsule/capsule/internal/webhook/utils" "github.com/projectcapsule/capsule/pkg/api/meta" "github.com/projectcapsule/capsule/pkg/configuration" "github.com/projectcapsule/capsule/pkg/utils/users" @@ -85,12 +84,7 @@ func (h *validating) handle( } // We don't want to allow promoted serviceaccounts to promote other serviceaccounts - allowed, err := users.IsTenantOwner(ctx, c, h.cfg, tnt, req.UserInfo) - if err != nil { - return utils.ErroredResponse(err) - } - - if allowed { + if ok := users.IsTenantOwnerByStatus(ctx, c, h.cfg, tnt, req.UserInfo); ok { return nil } diff --git a/internal/webhook/tenant/validation/warnings.go b/internal/webhook/tenant/validation/warnings.go index cda9219e..9cb805e2 100644 --- a/internal/webhook/tenant/validation/warnings.go +++ b/internal/webhook/tenant/validation/warnings.go @@ -14,17 +14,27 @@ import ( capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" capsulewebhook "github.com/projectcapsule/capsule/internal/webhook" "github.com/projectcapsule/capsule/internal/webhook/utils" + "github.com/projectcapsule/capsule/pkg/configuration" ) -type warningHandler struct{} - -func WarningHandler() capsulewebhook.Handler { - return &warningHandler{} +type warningHandler struct { + cfg configuration.Configuration } -func (h *warningHandler) OnCreate(_ client.Client, decoder admission.Decoder, _ record.EventRecorder) capsulewebhook.Func { - return func(_ context.Context, req admission.Request) *admission.Response { - return h.handle(decoder, req) +func WarningHandler(cfg configuration.Configuration) capsulewebhook.Handler { + return &warningHandler{ + cfg: cfg, + } +} + +func (h *warningHandler) OnCreate(c client.Client, decoder admission.Decoder, _ record.EventRecorder) capsulewebhook.Func { + return func(ctx context.Context, req admission.Request) *admission.Response { + tnt := &capsulev1beta2.Tenant{} + if err := decoder.Decode(req, tnt); err != nil { + return utils.ErroredResponse(err) + } + + return h.handle(tnt, decoder, req) } } @@ -36,16 +46,16 @@ func (h *warningHandler) OnDelete(client.Client, admission.Decoder, record.Event func (h *warningHandler) OnUpdate(_ client.Client, decoder admission.Decoder, _ record.EventRecorder) capsulewebhook.Func { return func(_ context.Context, req admission.Request) *admission.Response { - return h.handle(decoder, req) + tnt := &capsulev1beta2.Tenant{} + if err := decoder.Decode(req, tnt); err != nil { + return utils.ErroredResponse(err) + } + + return h.handle(tnt, decoder, req) } } -func (h *warningHandler) handle(decoder admission.Decoder, req admission.Request) *admission.Response { - tenant := &capsulev1beta2.Tenant{} - if err := decoder.Decode(req, tenant); err != nil { - return utils.ErroredResponse(err) - } - +func (h *warningHandler) handle(tnt *capsulev1beta2.Tenant, decoder admission.Decoder, req admission.Request) *admission.Response { response := &admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ UID: req.UID, @@ -53,31 +63,31 @@ func (h *warningHandler) handle(decoder admission.Decoder, req admission.Request }, } - if len(tenant.Spec.LimitRanges.Items) > 0 { + if len(tnt.Spec.LimitRanges.Items) > 0 { response.Warnings = append(response.Warnings, "Limitranges are deprecated and will be removed int the future. You need to consider to migrate to TenantReplications: https://projectcapsule.dev/docs/tenants/enforcement/#limitrange-distribution-with-tenantreplications.") } - if len(tenant.Spec.NetworkPolicies.Items) > 0 { + if len(tnt.Spec.NetworkPolicies.Items) > 0 { response.Warnings = append(response.Warnings, "NetworkPolicies are deprecated and will be removed int the future. You need to consider to migrate to TenantReplications: https://projectcapsule.dev/docs/tenants/enforcement/#networkpolicy-distribution-with-tenantreplications.") } - if tenant.Spec.NamespaceOptions != nil && tenant.Spec.NamespaceOptions.AdditionalMetadata != nil { + if tnt.Spec.NamespaceOptions != nil && tnt.Spec.NamespaceOptions.AdditionalMetadata != nil { response.Warnings = append(response.Warnings, "additionalMetadata is deprecated and will be removed int the future. You need to consider to migrate to AdditionalMetadataList: https://projectcapsule.dev/docs/tenants/enforcement/#additionalmetadatalist.") } - if tenant.Spec.StorageClasses != nil && tenant.Spec.StorageClasses.Regex != "" { + if tnt.Spec.StorageClasses != nil && tnt.Spec.StorageClasses.Regex != "" { response.Warnings = append(response.Warnings, "Using the regex property to select StorageClasses is deprecated and will be removed int the future.") } - if tenant.Spec.GatewayOptions.AllowedClasses != nil && tenant.Spec.GatewayOptions.AllowedClasses.Regex != "" { + if tnt.Spec.GatewayOptions.AllowedClasses != nil && tnt.Spec.GatewayOptions.AllowedClasses.Regex != "" { response.Warnings = append(response.Warnings, "Using the regex property to select GatewayClasses is deprecated and will be removed int the future.") } - if tenant.Spec.PriorityClasses != nil && tenant.Spec.PriorityClasses.Regex != "" { + if tnt.Spec.PriorityClasses != nil && tnt.Spec.PriorityClasses.Regex != "" { response.Warnings = append(response.Warnings, "Using the regex property to select PriorityClasses is deprecated and will be removed int the future.") } - if tenant.Spec.RuntimeClasses != nil && tenant.Spec.RuntimeClasses.Regex != "" { + if tnt.Spec.RuntimeClasses != nil && tnt.Spec.RuntimeClasses.Regex != "" { response.Warnings = append(response.Warnings, "Using the regex property to select RuntimeClasses is deprecated and will be removed int the future.") } diff --git a/pkg/api/misc/selectors.go b/pkg/api/misc/selectors.go new file mode 100644 index 00000000..d9e76490 --- /dev/null +++ b/pkg/api/misc/selectors.go @@ -0,0 +1,143 @@ +// Copyright 2020-2025 Project Capsule Authors +// SPDX-License-Identifier: Apache-2.0 + +package misc + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// Selector for resources and their labels or selecting origin namespaces +// +kubebuilder:object:generate=true +type NamespaceSelector struct { + // Select Items based on their labels. If the namespaceSelector is also set, the selector is applied + // to items within the selected namespaces. Otherwise for all the items. + *metav1.LabelSelector `json:",inline"` +} + +// GetMatchingNamespaces retrieves the list of namespaces that match the NamespaceSelector. +func (s *NamespaceSelector) GetMatchingNamespaces(ctx context.Context, client client.Client) ([]corev1.Namespace, error) { + if s.LabelSelector == nil { + return nil, nil // No namespace selector means all namespaces + } + + nsSelector, err := metav1.LabelSelectorAsSelector(s.LabelSelector) + if err != nil { + return nil, fmt.Errorf("invalid namespace selector: %w", err) + } + + namespaceList := &corev1.NamespaceList{} + if err := client.List(ctx, namespaceList); err != nil { + return nil, fmt.Errorf("failed to list namespaces: %w", err) + } + + var matchingNamespaces []corev1.Namespace + + for _, ns := range namespaceList.Items { + if nsSelector.Matches(labels.Set(ns.Labels)) { + matchingNamespaces = append(matchingNamespaces, ns) + } + } + + return matchingNamespaces, nil +} + +// ListBySelectors lists objects of type T (using list L), then returns all items that +// match ANY of the provided LabelSelectors. The result is unique by namespace/name. +func ListBySelectors[T client.Object]( + ctx context.Context, + c client.Client, + list client.ObjectList, + selectors []*metav1.LabelSelector, +) ([]T, error) { + if len(selectors) == 0 { + return nil, nil + } + + if list == nil { + return nil, fmt.Errorf("list must not be nil") + } + + // Preallocate with upper bound (len(selectors)); nil selectors will just not be used. + selList := make([]labels.Selector, 0, len(selectors)) + + for _, ls := range selectors { + if ls == nil { + continue + } + + sel, err := metav1.LabelSelectorAsSelector(ls) + if err != nil { + return nil, fmt.Errorf("invalid label selector %v: %w", ls, err) + } + + selList = append(selList, sel) + } + + if len(selList) == 0 { + return nil, nil + } + + // List all objects once + if err := c.List(ctx, list); err != nil { + return nil, fmt.Errorf("listing objects: %w", err) + } + + rawItems, err := meta.ExtractList(list) + if err != nil { + return nil, fmt.Errorf("extracting list items: %w", err) + } + + // Deduplicate by namespace/name + seen := make(map[client.ObjectKey]struct{}, len(rawItems)) + + // Upper bound: at most len(rawItems) will match; good enough for prealloc. + result := make([]T, 0, len(rawItems)) + + for _, obj := range rawItems { + typed, ok := obj.(T) + if !ok { + continue + } + + lbls := typed.GetLabels() + if len(lbls) == 0 { + continue + } + + set := labels.Set(lbls) + + // Match against ANY selector + matched := false + + for _, sel := range selList { + if sel.Matches(set) { + matched = true + + break + } + } + + if !matched { + continue + } + + key := client.ObjectKeyFromObject(typed) + if _, exists := seen[key]; exists { + continue + } + + seen[key] = struct{}{} + + result = append(result, typed) + } + + return result, nil +} diff --git a/pkg/api/misc/zz_generated.deepcopy.go b/pkg/api/misc/zz_generated.deepcopy.go new file mode 100644 index 00000000..21db5fa4 --- /dev/null +++ b/pkg/api/misc/zz_generated.deepcopy.go @@ -0,0 +1,32 @@ +//go:build !ignore_autogenerated + +// Copyright 2020-2023 Project Capsule Authors. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by controller-gen. DO NOT EDIT. + +package misc + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NamespaceSelector) DeepCopyInto(out *NamespaceSelector) { + *out = *in + if in.LabelSelector != nil { + in, out := &in.LabelSelector, &out.LabelSelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceSelector. +func (in *NamespaceSelector) DeepCopy() *NamespaceSelector { + if in == nil { + return nil + } + out := new(NamespaceSelector) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/api/owner.go b/pkg/api/owner.go index 5037a26a..4a59909e 100644 --- a/pkg/api/owner.go +++ b/pkg/api/owner.go @@ -6,11 +6,8 @@ package api // +kubebuilder:object:generate=true type OwnerSpec struct { - UserSpec `json:",inline"` + CoreOwnerSpec `json:",inline"` - // Defines additional cluster-roles for the specific Owner. - // +kubebuilder:default={admin,capsule-namespace-deleter} - ClusterRoles []string `json:"clusterRoles,omitempty"` // Proxy settings for tenant owner. ProxyOperations []ProxySettings `json:"proxySettings,omitempty"` // Additional Labels for the synchronized rolebindings @@ -19,6 +16,16 @@ type OwnerSpec struct { Annotations map[string]string `json:"annotations,omitempty"` } +// +kubebuilder:object:generate=true + +type CoreOwnerSpec struct { + UserSpec `json:",inline"` + + // Defines additional cluster-roles for the specific Owner. + // +kubebuilder:default={admin,capsule-namespace-deleter} + ClusterRoles []string `json:"clusterRoles,omitempty"` +} + // +kubebuilder:validation:Enum=User;Group;ServiceAccount type OwnerKind string diff --git a/pkg/api/owner_list.go b/pkg/api/owner_list.go index 3853fab4..8c801abc 100644 --- a/pkg/api/owner_list.go +++ b/pkg/api/owner_list.go @@ -1,7 +1,6 @@ // Copyright 2020-2025 Project Capsule Authors // SPDX-License-Identifier: Apache-2.0 -//nolint:dupl package api import ( @@ -31,6 +30,15 @@ func (o OwnerListSpec) IsOwner(name string, groups []string) bool { return false } +func (o OwnerListSpec) ToStatusOwners() OwnerStatusListSpec { + list := OwnerStatusListSpec{} + for _, owner := range o { + list = append(list, owner.CoreOwnerSpec) + } + + return list +} + func (o OwnerListSpec) FindOwner(name string, kind OwnerKind) (owner OwnerSpec) { sort.Sort(ByKindAndName(o)) i := sort.Search(len(o), func(i int) bool { diff --git a/pkg/api/owner_list_test.go b/pkg/api/owner_list_test.go index b7d5f9d5..06cc7f0c 100644 --- a/pkg/api/owner_list_test.go +++ b/pkg/api/owner_list_test.go @@ -11,9 +11,11 @@ import ( func TestOwnerListSpec_FindOwner(t *testing.T) { bla := OwnerSpec{ - UserSpec: UserSpec{ - Kind: UserOwner, - Name: "bla", + CoreOwnerSpec: CoreOwnerSpec{ + UserSpec: UserSpec{ + Kind: UserOwner, + Name: "bla", + }, }, ProxyOperations: []ProxySettings{ { @@ -23,9 +25,11 @@ func TestOwnerListSpec_FindOwner(t *testing.T) { }, } bar := OwnerSpec{ - UserSpec: UserSpec{ - Kind: GroupOwner, - Name: "bar", + CoreOwnerSpec: CoreOwnerSpec{ + UserSpec: UserSpec{ + Kind: GroupOwner, + Name: "bar", + }, }, ProxyOperations: []ProxySettings{ { @@ -35,9 +39,11 @@ func TestOwnerListSpec_FindOwner(t *testing.T) { }, } baz := OwnerSpec{ - UserSpec: UserSpec{ - Kind: UserOwner, - Name: "baz", + CoreOwnerSpec: CoreOwnerSpec{ + UserSpec: UserSpec{ + Kind: UserOwner, + Name: "baz", + }, }, ProxyOperations: []ProxySettings{ { @@ -47,9 +53,11 @@ func TestOwnerListSpec_FindOwner(t *testing.T) { }, } fim := OwnerSpec{ - UserSpec: UserSpec{ - Kind: ServiceAccountOwner, - Name: "fim", + CoreOwnerSpec: CoreOwnerSpec{ + UserSpec: UserSpec{ + Kind: ServiceAccountOwner, + Name: "fim", + }, }, ProxyOperations: []ProxySettings{ { @@ -59,9 +67,11 @@ func TestOwnerListSpec_FindOwner(t *testing.T) { }, } bom := OwnerSpec{ - UserSpec: UserSpec{ - Kind: GroupOwner, - Name: "bom", + CoreOwnerSpec: CoreOwnerSpec{ + UserSpec: UserSpec{ + Kind: GroupOwner, + Name: "bom", + }, }, ProxyOperations: []ProxySettings{ { @@ -75,9 +85,11 @@ func TestOwnerListSpec_FindOwner(t *testing.T) { }, } qip := OwnerSpec{ - UserSpec: UserSpec{ - Kind: ServiceAccountOwner, - Name: "qip", + CoreOwnerSpec: CoreOwnerSpec{ + UserSpec: UserSpec{ + Kind: ServiceAccountOwner, + Name: "qip", + }, }, ProxyOperations: []ProxySettings{ { diff --git a/pkg/api/owner_status_list.go b/pkg/api/owner_status_list.go new file mode 100644 index 00000000..7297aeff --- /dev/null +++ b/pkg/api/owner_status_list.go @@ -0,0 +1,136 @@ +// Copyright 2020-2025 Project Capsule Authors +// SPDX-License-Identifier: Apache-2.0 + +package api + +import ( + "sort" +) + +// +kubebuilder:object:generate=true + +type OwnerStatusListSpec []CoreOwnerSpec + +func (o *OwnerStatusListSpec) Upsert( + newOwner CoreOwnerSpec, +) { + owners := *o + + // Ensure slice is sorted before binary search + sort.Sort(GetByKindAndName(owners)) + + less := func(a, b CoreOwnerSpec) bool { + if a.Kind.String() != b.Kind.String() { + return a.Kind.String() < b.Kind.String() + } + + return a.Name < b.Name + } + + // Find the first index where owners[i] >= newOwner + idx := sort.Search(len(owners), func(i int) bool { + return !less(owners[i], newOwner) + }) + + // If we found an exact match (same Kind + Name), merge ClusterRoles + if idx < len(owners) && !less(owners[idx], newOwner) && !less(newOwner, owners[idx]) { + existing := &owners[idx] + + roleSet := make(map[string]struct{}, len(existing.ClusterRoles)) + for _, r := range existing.ClusterRoles { + roleSet[r] = struct{}{} + } + + for _, r := range newOwner.ClusterRoles { + if _, ok := roleSet[r]; !ok { + existing.ClusterRoles = append(existing.ClusterRoles, r) + roleSet[r] = struct{}{} + } + } + + *o = owners + + return + } + + // Not found: append and keep sorted + owners = append(owners, newOwner) + sort.Sort(GetByKindAndName(owners)) + *o = owners +} + +func (o OwnerStatusListSpec) IsOwner(name string, groups []string) bool { + var groupSet map[string]struct{} + if len(groups) > 0 { + groupSet = make(map[string]struct{}, len(groups)) + for _, g := range groups { + groupSet[g] = struct{}{} + } + } + + for _, owner := range o { + switch owner.Kind { + case UserOwner, ServiceAccountOwner: + if name == owner.Name { + return true + } + case GroupOwner: + if groupSet == nil { + continue + } + + if _, ok := groupSet[owner.Name]; ok { + return true + } + } + } + + return false +} + +func (o OwnerStatusListSpec) FindOwner(name string, kind OwnerKind) (CoreOwnerSpec, bool) { + // Sort in-place by (Kind.String(), Name). + sort.Sort(GetByKindAndName(o)) + + targetKind := kind.String() + n := len(o) + + idx := sort.Search(n, func(i int) bool { + ki := o[i].Kind.String() + + switch { + case ki > targetKind: + return true + case ki < targetKind: + return false + default: + return o[i].Name >= name + } + }) + + if idx < n && + o[idx].Kind.String() == targetKind && + o[idx].Name == name { + return o[idx], true + } + + return CoreOwnerSpec{}, false +} + +type GetByKindAndName OwnerStatusListSpec + +func (b GetByKindAndName) Len() int { + return len(b) +} + +func (b GetByKindAndName) Less(i, j int) bool { + if b[i].Kind.String() != b[j].Kind.String() { + return b[i].Kind.String() < b[j].Kind.String() + } + + return b[i].Name < b[j].Name +} + +func (b GetByKindAndName) Swap(i, j int) { + b[i], b[j] = b[j], b[i] +} diff --git a/pkg/api/owner_status_list_test.go b/pkg/api/owner_status_list_test.go new file mode 100644 index 00000000..78ccd343 --- /dev/null +++ b/pkg/api/owner_status_list_test.go @@ -0,0 +1,355 @@ +// Copyright 2020-2025 Project Capsule Authors +// SPDX-License-Identifier: Apache-2.0 + +package api_test + +import ( + "math/rand" + "reflect" + "sort" + "testing" + "time" + + "github.com/projectcapsule/capsule/pkg/api" +) + +func slowIsOwner(o api.OwnerStatusListSpec, name string, groups []string) bool { + for _, owner := range o { + switch owner.Kind { + case api.UserOwner, api.ServiceAccountOwner: + if name == owner.Name { + return true + } + case api.GroupOwner: + for _, group := range groups { + if group == owner.Name { + return true + } + } + } + } + return false +} + +// linearFind is the obvious, slow, but correct reference implementation. +func linearFind(o api.OwnerStatusListSpec, name string, kind api.OwnerKind) (api.CoreOwnerSpec, bool) { + for _, x := range o { + if x.Kind == kind && x.Name == name { + return x, true + } + } + return api.CoreOwnerSpec{}, false +} + +// randomName generates a simple lowercase name of length n. +func randomName(rnd *rand.Rand, n int) string { + const letters = "abcdefghijklmnopqrstuvwxyz" + b := make([]byte, n) + for i := range b { + b[i] = letters[rnd.Intn(len(letters))] + } + return string(b) +} + +func TestUpsert_AddsNewOwnerToEmptyList(t *testing.T) { + var list api.OwnerStatusListSpec + + list.Upsert(api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Kind: api.UserOwner, + Name: "alice", + }, + ClusterRoles: []string{"admin"}, + }) + + if len(list) != 1 { + t.Fatalf("expected 1 owner, got %d", len(list)) + } + got := list[0] + if got.Kind != api.UserOwner || got.Name != "alice" { + t.Fatalf("unexpected owner: %+v", got) + } + if !reflect.DeepEqual(got.ClusterRoles, []string{"admin"}) { + t.Fatalf("unexpected roles: %#v", got.ClusterRoles) + } +} + +func TestUpsert_MergesClusterRolesForExistingOwner(t *testing.T) { + list := api.OwnerStatusListSpec{ + { + UserSpec: api.UserSpec{ + Kind: api.UserOwner, + Name: "alice", + }, + ClusterRoles: []string{"admin", "capsule-namespace-deleter"}, + }, + } + + list.Upsert(api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Kind: api.UserOwner, + Name: "alice", + }, + ClusterRoles: []string{"extra-sad"}, + }) + + if len(list) != 1 { + t.Fatalf("expected 1 owner, got %d", len(list)) + } + got := list[0] + if got.Kind != api.UserOwner || got.Name != "alice" { + t.Fatalf("unexpected owner: %+v", got) + } + + // Roles should be union of both sets, order: existing roles first, then new ones + expected := []string{"admin", "capsule-namespace-deleter", "extra-sad"} + if !reflect.DeepEqual(got.ClusterRoles, expected) { + t.Fatalf("expected roles %v, got %v", expected, got.ClusterRoles) + } +} + +func TestUpsert_DeduplicatesClusterRoles(t *testing.T) { + list := api.OwnerStatusListSpec{ + { + UserSpec: api.UserSpec{ + Kind: api.UserOwner, + Name: "alice", + }, + ClusterRoles: []string{"admin", "viewer"}, + }, + } + + list.Upsert(api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Kind: api.UserOwner, + Name: "alice", + }, + ClusterRoles: []string{"viewer", "editor"}, + }) + + if len(list) != 1 { + t.Fatalf("expected 1 owner, got %d", len(list)) + } + got := list[0] + + expected := []string{"admin", "viewer", "editor"} + if !reflect.DeepEqual(got.ClusterRoles, expected) { + t.Fatalf("expected roles %v, got %v", expected, got.ClusterRoles) + } +} + +func TestUpsert_KeepsListSortedAndMergesIntoExistingInUnsortedInitialSlice(t *testing.T) { + // Start with an unsorted slice, as could come from API/server + list := api.OwnerStatusListSpec{ + { + UserSpec: api.UserSpec{ + Kind: api.UserOwner, + Name: "bob", + }, + ClusterRoles: []string{"bob-role"}, + }, + { + UserSpec: api.UserSpec{ + Kind: api.UserOwner, + Name: "alice", + }, + ClusterRoles: []string{"admin"}, + }, + } + + // Upsert another alice + list.Upsert(api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Kind: api.UserOwner, + Name: "alice", + }, + ClusterRoles: []string{"extra"}, + }) + + if len(list) != 2 { + t.Fatalf("expected 2 owners (alice, bob), got %d", len(list)) + } + + // Ensure sorted by Kind.Name: alice before bob + // (relies on ByKindAndName order) + sorted := make(api.OwnerStatusListSpec, len(list)) + copy(sorted, list) + sort.Sort(api.GetByKindAndName(sorted)) + + if !reflect.DeepEqual(list, sorted) { + t.Fatalf("expected list to be sorted by kind+name, got %#v", list) + } + + // Find alice and check roles + var alice *api.CoreOwnerSpec + for i := range list { + if list[i].Name == "alice" { + alice = &list[i] + break + } + } + if alice == nil { + t.Fatalf("alice not found in list") + } + + expectedRoles := []string{"admin", "extra"} + if !reflect.DeepEqual(alice.ClusterRoles, expectedRoles) { + t.Fatalf("expected alice roles %v, got %v", expectedRoles, alice.ClusterRoles) + } +} + +func TestGetByKindAndNameOrdering(t *testing.T) { + o := api.OwnerStatusListSpec{ + api.CoreOwnerSpec{UserSpec: api.UserSpec{Name: "b", Kind: api.ServiceAccountOwner}}, + api.CoreOwnerSpec{UserSpec: api.UserSpec{Name: "z", Kind: api.UserOwner}}, + api.CoreOwnerSpec{UserSpec: api.UserSpec{Name: "a", Kind: api.GroupOwner}}, + api.CoreOwnerSpec{UserSpec: api.UserSpec{Name: "a", Kind: api.UserOwner}}, + } + + // Sort using production ordering + got := append(api.OwnerStatusListSpec(nil), o...) + sort.Sort(api.GetByKindAndName(got)) + + // Manually sorted expectation using the same logic. + want := append(api.OwnerStatusListSpec(nil), o...) + sort.Slice(want, func(i, j int) bool { + if want[i].Kind.String() != want[j].Kind.String() { + return want[i].Kind.String() < want[j].Kind.String() + } + return want[i].Name < want[j].Name + }) + + if len(got) != len(want) { + t.Fatalf("length mismatch: got %d, want %d", len(got), len(want)) + } + for i := range got { + if !reflect.DeepEqual(got[i], want[i]) { + t.Fatalf("ordering mismatch at %d: got %+v, want %+v", i, got[i], want[i]) + } + } +} + +func TestFindOwner_Randomized(t *testing.T) { + rnd := rand.New(rand.NewSource(42)) // fixed seed for deterministic test runs + + ownerKinds := []api.OwnerKind{ + api.GroupOwner, + api.UserOwner, + api.ServiceAccountOwner, + } + + const ( + numLists = 200 + maxLength = 40 + numLookupsPerList = 80 + ) + + for listIdx := 0; listIdx < numLists; listIdx++ { + var list api.OwnerStatusListSpec + n := rnd.Intn(maxLength) + for i := 0; i < n; i++ { + k := ownerKinds[rnd.Intn(len(ownerKinds))] + list = append(list, api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: randomName(rnd, 3+rnd.Intn(4)), // length 3–6 + Kind: k, + }, + }) + } + + for lookupIdx := 0; lookupIdx < numLookupsPerList; lookupIdx++ { + var qName string + var qKind api.OwnerKind + + if len(list) > 0 && rnd.Float64() < 0.6 { + // 60% of lookups: pick a real element, must be found + pick := list[rnd.Intn(len(list))] + qName = pick.Name + qKind = pick.Kind + } else { + // 40%: random query, may or may not exist + qName = randomName(rnd, 3+rnd.Intn(4)) + qKind = ownerKinds[rnd.Intn(len(ownerKinds))] + } + + listCopy := append(api.OwnerStatusListSpec(nil), list...) + gotOwner, gotFound := listCopy.FindOwner(qName, qKind) + wantOwner, wantFound := linearFind(list, qName, qKind) + + if gotFound != wantFound { + t.Fatalf("list=%d lookup=%d: found mismatch for (%q,%v): got=%v, want=%v", + listIdx, lookupIdx, qName, qKind, gotFound, wantFound) + } + if gotFound && !reflect.DeepEqual(gotOwner, wantOwner) { + t.Fatalf("list=%d lookup=%d: owner mismatch for (%q,%v):\n got= %+v\nwant= %+v", + listIdx, lookupIdx, qName, qKind, gotOwner, wantOwner) + } + } + } +} + +func TestIsOwner_RandomizedMatchesSlowImplementation(t *testing.T) { + rnd := rand.New(rand.NewSource(time.Now().UnixNano())) + + ownerKinds := []api.OwnerKind{ + api.UserOwner, + api.GroupOwner, + api.ServiceAccountOwner, + } + + const ( + numLists = 200 + maxOwnersPerList = 30 + numLookupsPerList = 80 + maxGroupsPerUser = 10 + ) + + for listIdx := 0; listIdx < numLists; listIdx++ { + // Generate a random owner list (possibly with duplicates). + var owners api.OwnerStatusListSpec + nOwners := rnd.Intn(maxOwnersPerList) + for i := 0; i < nOwners; i++ { + kind := ownerKinds[rnd.Intn(len(ownerKinds))] + owners = append(owners, api.CoreOwnerSpec{ + UserSpec: api.UserSpec{ + Name: randomName(rnd, 3+rnd.Intn(4)), // length 3–6 + Kind: kind, + }, + }) + } + + for lookupIdx := 0; lookupIdx < numLookupsPerList; lookupIdx++ { + // Generate a random userName and groups, + // sometimes biased to hit existing owners/groups. + var userName string + var groups []string + + // 50% of the time: pick an existing owner name as userName + if len(owners) > 0 && rnd.Float64() < 0.5 { + pick := owners[rnd.Intn(len(owners))] + userName = pick.Name + } else { + userName = randomName(rnd, 3+rnd.Intn(4)) + } + + // Random groups, sometimes including owner names + nGroups := rnd.Intn(maxGroupsPerUser) + for i := 0; i < nGroups; i++ { + if len(owners) > 0 && rnd.Float64() < 0.5 { + pick := owners[rnd.Intn(len(owners))] + groups = append(groups, pick.Name) + } else { + groups = append(groups, randomName(rnd, 3+rnd.Intn(4))) + } + } + + got := owners.IsOwner(userName, groups) + want := slowIsOwner(owners, userName, groups) + + if got != want { + t.Fatalf("list=%d lookup=%d: mismatch\n owners=%v\n user=%q\n groups=%v\n optimized=%v\n slow=%v", + listIdx, lookupIdx, owners, userName, groups, got, want) + } + } + } +} diff --git a/internal/controllers/rbac/const.go b/pkg/api/rbac.go similarity index 89% rename from internal/controllers/rbac/const.go rename to pkg/api/rbac.go index 0bf129b7..89178b6c 100644 --- a/internal/controllers/rbac/const.go +++ b/pkg/api/rbac.go @@ -1,7 +1,7 @@ // Copyright 2020-2025 Project Capsule Authors // SPDX-License-Identifier: Apache-2.0 -package rbac +package api import ( rbacv1 "k8s.io/api/rbac/v1" @@ -14,7 +14,7 @@ const ( ) var ( - clusterRoles = map[string]*rbacv1.ClusterRole{ + ClusterRoles = map[string]*rbacv1.ClusterRole{ ProvisionerRoleName: { ObjectMeta: metav1.ObjectMeta{ Name: ProvisionerRoleName, @@ -41,7 +41,7 @@ var ( }, } - provisionerClusterRoleBinding = &rbacv1.ClusterRoleBinding{ + ProvisionerClusterRoleBinding = &rbacv1.ClusterRoleBinding{ ObjectMeta: metav1.ObjectMeta{ Name: ProvisionerRoleName, }, diff --git a/pkg/api/selectors.go b/pkg/api/selectors.go deleted file mode 100644 index 7d72ad6f..00000000 --- a/pkg/api/selectors.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2020-2025 Project Capsule Authors -// SPDX-License-Identifier: Apache-2.0 - -package api - -import ( - "context" - "fmt" - - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// Selector for resources and their labels or selecting origin namespaces -// +kubebuilder:object:generate=true -type NamespaceSelector struct { - // Select Items based on their labels. If the namespaceSelector is also set, the selector is applied - // to items within the selected namespaces. Otherwise for all the items. - *metav1.LabelSelector `json:",inline"` -} - -// GetMatchingNamespaces retrieves the list of namespaces that match the NamespaceSelector. -func (s *NamespaceSelector) GetMatchingNamespaces(ctx context.Context, client client.Client) ([]corev1.Namespace, error) { - if s.LabelSelector == nil { - return nil, nil // No namespace selector means all namespaces - } - - nsSelector, err := metav1.LabelSelectorAsSelector(s.LabelSelector) - if err != nil { - return nil, fmt.Errorf("invalid namespace selector: %w", err) - } - - namespaceList := &corev1.NamespaceList{} - if err := client.List(ctx, namespaceList); err != nil { - return nil, fmt.Errorf("failed to list namespaces: %w", err) - } - - var matchingNamespaces []corev1.Namespace - - for _, ns := range namespaceList.Items { - if nsSelector.Matches(labels.Set(ns.Labels)) { - matchingNamespaces = append(matchingNamespaces, ns) - } - } - - return matchingNamespaces, nil -} diff --git a/pkg/api/users_list.go b/pkg/api/users_list.go index 8bf88816..4913dd64 100644 --- a/pkg/api/users_list.go +++ b/pkg/api/users_list.go @@ -1,7 +1,6 @@ // Copyright 2020-2025 Project Capsule Authors // SPDX-License-Identifier: Apache-2.0 -//nolint:dupl package api import ( diff --git a/pkg/api/zz_generated.deepcopy.go b/pkg/api/zz_generated.deepcopy.go index 850cfb32..2c2ee137 100644 --- a/pkg/api/zz_generated.deepcopy.go +++ b/pkg/api/zz_generated.deepcopy.go @@ -161,6 +161,27 @@ func (in *AllowedServices) DeepCopy() *AllowedServices { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CoreOwnerSpec) DeepCopyInto(out *CoreOwnerSpec) { + *out = *in + out.UserSpec = in.UserSpec + if in.ClusterRoles != nil { + in, out := &in.ClusterRoles, &out.ClusterRoles + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CoreOwnerSpec. +func (in *CoreOwnerSpec) DeepCopy() *CoreOwnerSpec { + if in == nil { + return nil + } + out := new(CoreOwnerSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DefaultAllowedListSpec) DeepCopyInto(out *DefaultAllowedListSpec) { *out = *in @@ -239,26 +260,6 @@ func (in *LimitRangesSpec) DeepCopy() *LimitRangesSpec { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *NamespaceSelector) DeepCopyInto(out *NamespaceSelector) { - *out = *in - if in.LabelSelector != nil { - in, out := &in.LabelSelector, &out.LabelSelector - *out = new(v1.LabelSelector) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceSelector. -func (in *NamespaceSelector) DeepCopy() *NamespaceSelector { - if in == nil { - return nil - } - out := new(NamespaceSelector) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NetworkPolicySpec) DeepCopyInto(out *NetworkPolicySpec) { *out = *in @@ -305,12 +306,7 @@ func (in OwnerListSpec) DeepCopy() OwnerListSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OwnerSpec) DeepCopyInto(out *OwnerSpec) { *out = *in - out.UserSpec = in.UserSpec - if in.ClusterRoles != nil { - in, out := &in.ClusterRoles, &out.ClusterRoles - *out = make([]string, len(*in)) - copy(*out, *in) - } + in.CoreOwnerSpec.DeepCopyInto(&out.CoreOwnerSpec) if in.ProxyOperations != nil { in, out := &in.ProxyOperations, &out.ProxyOperations *out = make([]ProxySettings, len(*in)) @@ -344,6 +340,27 @@ func (in *OwnerSpec) DeepCopy() *OwnerSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in OwnerStatusListSpec) DeepCopyInto(out *OwnerStatusListSpec) { + { + in := &in + *out = make(OwnerStatusListSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OwnerStatusListSpec. +func (in OwnerStatusListSpec) DeepCopy() OwnerStatusListSpec { + if in == nil { + return nil + } + out := new(OwnerStatusListSpec) + in.DeepCopyInto(out) + return *out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PodOptions) DeepCopyInto(out *PodOptions) { *out = *in diff --git a/pkg/utils/maps_test.go b/pkg/utils/maps_test.go index 9bb2ad55..73ccac12 100644 --- a/pkg/utils/maps_test.go +++ b/pkg/utils/maps_test.go @@ -1,3 +1,6 @@ +// Copyright 2020-2025 Project Capsule Authors +// SPDX-License-Identifier: Apache-2.0 + package utils import ( diff --git a/pkg/utils/tenant/get_by.go b/pkg/utils/tenant/get_by.go index 63ad9b7e..2e3f6590 100644 --- a/pkg/utils/tenant/get_by.go +++ b/pkg/utils/tenant/get_by.go @@ -167,12 +167,7 @@ func GetTenantByLabelsAndUser( } if tnt != nil { - ok, err := users.IsTenantOwner(ctx, c, cfg, tnt, userInfo) - if err != nil { - return nil, err - } - - if !ok { + if ok := users.IsTenantOwnerByStatus(ctx, c, cfg, tnt, userInfo); !ok { return nil, fmt.Errorf("can not assign the desired namespace to a non-owned Tenant") } diff --git a/pkg/utils/tenant/owned.go b/pkg/utils/tenant/owned.go index 88a94fde..06dbbb8d 100644 --- a/pkg/utils/tenant/owned.go +++ b/pkg/utils/tenant/owned.go @@ -22,14 +22,14 @@ func NamespaceIsOwned( ns *corev1.Namespace, tnt *capsulev1beta2.Tenant, userInfo authenticationv1.UserInfo, -) (bool, error) { +) bool { for _, ownerRef := range ns.OwnerReferences { if !IsTenantOwnerReferenceForTenant(ownerRef, tnt) { continue } - return users.IsTenantOwner(ctx, c, cfg, tnt, userInfo) + return users.IsTenantOwnerByStatus(ctx, c, cfg, tnt, userInfo) } - return false, nil + return false } diff --git a/pkg/utils/users/is_tenant_owner.go b/pkg/utils/users/is_tenant_owner.go index 6a0908b1..0b34f70a 100644 --- a/pkg/utils/users/is_tenant_owner.go +++ b/pkg/utils/users/is_tenant_owner.go @@ -21,13 +21,33 @@ func IsTenantOwner( ctx context.Context, c client.Client, cfg configuration.Configuration, - tenant *capsulev1beta2.Tenant, + tnt *capsulev1beta2.Tenant, userInfo authenticationv1.UserInfo, ) (bool, error) { - if isOwner := tenant.Spec.Owners.IsOwner(userInfo.Username, userInfo.Groups); isOwner { + if isOwner := tnt.Spec.Owners.IsOwner(userInfo.Username, userInfo.Groups); isOwner { return true, nil } + return IsCommonOwner(ctx, c, cfg, tnt, userInfo) +} + +func IsTenantOwnerByStatus( + ctx context.Context, + c client.Client, + cfg configuration.Configuration, + tnt *capsulev1beta2.Tenant, + userInfo authenticationv1.UserInfo, +) bool { + return tnt.Status.Owners.IsOwner(userInfo.Username, userInfo.Groups) +} + +func IsCommonOwner( + ctx context.Context, + c client.Client, + cfg configuration.Configuration, + tnt *capsulev1beta2.Tenant, + userInfo authenticationv1.UserInfo, +) (bool, error) { // Administrators are always Owners if cfg.Administrators().IsPresent(userInfo.Username, userInfo.Groups) { return true, nil diff --git a/pkg/utils/users/serviceaccounts.go b/pkg/utils/users/serviceaccounts.go index aae258cf..e47eac0e 100644 --- a/pkg/utils/users/serviceaccounts.go +++ b/pkg/utils/users/serviceaccounts.go @@ -34,7 +34,7 @@ func ResolveServiceAccountActor( sa := &corev1.ServiceAccount{} if err = c.Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, sa); err != nil { if apierrors.IsNotFound(err) { - return tnt, err + return nil, nil } return tnt, err