feat(config): administrators get delete privileges for tenant namespaces (#1749)

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
This commit is contained in:
Oliver Bähler
2025-11-20 18:33:08 +01:00
committed by GitHub
parent 581a8fe60e
commit 7e7d9d02c6
10 changed files with 137 additions and 47 deletions
+2 -2
View File
@@ -23,11 +23,11 @@ type Manager struct {
Log logr.Logger
}
func (c *Manager) SetupWithManager(mgr ctrl.Manager, configurationName string) error {
func (c *Manager) SetupWithManager(mgr ctrl.Manager, ctrlConfig utils.ControllerOptions) error {
c.client = mgr.GetClient()
return ctrl.NewControllerManagedBy(mgr).
For(&capsulev1beta2.CapsuleConfiguration{}, utils.NamesMatchingPredicate(configurationName)).
For(&capsulev1beta2.CapsuleConfiguration{}, utils.NamesMatchingPredicate(ctrlConfig.ConfigurationName)).
Complete(c)
}
+2 -2
View File
@@ -37,7 +37,7 @@ type Manager struct {
}
//nolint:revive
func (r *Manager) SetupWithManager(ctx context.Context, mgr ctrl.Manager, configurationName string) (err error) {
func (r *Manager) SetupWithManager(ctx context.Context, mgr ctrl.Manager, ctrlConfig utils.ControllerOptions) (err error) {
namesPredicate := utils.NamesMatchingPredicate(ProvisionerRoleName, DeleterRoleName)
crErr := ctrl.NewControllerManagedBy(mgr).
@@ -51,7 +51,7 @@ func (r *Manager) SetupWithManager(ctx context.Context, mgr ctrl.Manager, config
For(&rbacv1.ClusterRoleBinding{}, namesPredicate).
Watches(&capsulev1beta2.CapsuleConfiguration{}, handler.Funcs{
UpdateFunc: func(ctx context.Context, updateEvent event.TypedUpdateEvent[client.Object], limitingInterface workqueue.TypedRateLimitingInterface[reconcile.Request]) {
if updateEvent.ObjectNew.GetName() == configurationName {
if updateEvent.ObjectNew.GetName() == ctrlConfig.ConfigurationName {
if crbErr := r.EnsureClusterRoleBindingsProvisioner(ctx); crbErr != nil {
r.Log.Error(err, "cannot update ClusterRoleBinding upon CapsuleConfiguration update")
}
+15 -6
View File
@@ -18,6 +18,7 @@ import (
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/retry"
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/handler"
@@ -27,26 +28,34 @@ import (
"github.com/projectcapsule/capsule/internal/controllers/utils"
"github.com/projectcapsule/capsule/internal/metrics"
meta "github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/configuration"
)
type Manager struct {
client.Client
Metrics *metrics.TenantRecorder
Log logr.Logger
Recorder record.EventRecorder
RESTConfig *rest.Config
Metrics *metrics.TenantRecorder
Log logr.Logger
Recorder record.EventRecorder
Configuration configuration.Configuration
RESTConfig *rest.Config
}
func (r *Manager) SetupWithManager(mgr ctrl.Manager, cfg utils.ControllerOptions) error {
func (r *Manager) SetupWithManager(mgr ctrl.Manager, ctrlConfig utils.ControllerOptions) error {
return ctrl.NewControllerManagedBy(mgr).
For(&capsulev1beta2.Tenant{}).
Owns(&networkingv1.NetworkPolicy{}).
Owns(&corev1.LimitRange{}).
Owns(&corev1.ResourceQuota{}).
Owns(&rbacv1.RoleBinding{}).
Watches(
&capsulev1beta2.CapsuleConfiguration{},
handler.EnqueueRequestsFromMapFunc(r.enqueueAllTenants),
utils.NamesMatchingPredicate(ctrlConfig.ConfigurationName),
builder.WithPredicates(utils.CapsuleConfigSpecChangedPredicate),
).
Watches(&corev1.Namespace{}, handler.EnqueueRequestForOwner(mgr.GetScheme(), mgr.GetRESTMapper(), &capsulev1beta2.Tenant{})).
WithOptions(controller.Options{MaxConcurrentReconciles: cfg.MaxConcurrentReconciles}).
WithOptions(controller.Options{MaxConcurrentReconciles: ctrlConfig.MaxConcurrentReconciles}).
Complete(r)
}
+26 -3
View File
@@ -15,6 +15,7 @@ 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"
)
@@ -22,6 +23,20 @@ import (
// 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 {
var subject rbacv1.Subject
if owner.Kind == "ServiceAccount" {
@@ -45,8 +60,6 @@ func (r *Manager) ownerClusterRoleBindings(owner api.OwnerSpec, clusterRole stri
Subjects: []rbacv1.Subject{
subject,
},
Labels: owner.Labels,
Annotations: owner.Annotations,
}
}
@@ -80,6 +93,12 @@ 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 {
@@ -98,7 +117,7 @@ func (r *Manager) syncAdditionalRoleBinding(ctx context.Context, tenant *capsule
return err
}
var roleBindings []api.AdditionalRoleBindingsSpec
roleBindings := make([]api.AdditionalRoleBindingsSpec, 0)
for _, owner := range tenant.Spec.Owners {
for _, clusterRoleName := range owner.ClusterRoles {
@@ -106,6 +125,10 @@ func (r *Manager) syncAdditionalRoleBinding(ctx context.Context, tenant *capsule
}
}
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 {
+23
View File
@@ -10,13 +10,36 @@ import (
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
"github.com/projectcapsule/capsule/pkg/utils"
)
func (r *Manager) enqueueAllTenants(ctx context.Context, _ client.Object) []reconcile.Request {
var tenants capsulev1beta2.TenantList
if err := r.List(ctx, &tenants); err != nil {
r.Log.Error(err, "failed to list Tenants for class event")
return nil
}
reqs := make([]reconcile.Request, 0, len(tenants.Items))
for i := range tenants.Items {
reqs = append(reqs, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: tenants.Items[i].Name,
},
})
}
return reqs
}
// pruningResources is taking care of removing the no more requested sub-resources as LimitRange, ResourceQuota or
// NetworkPolicy using the "exists" and "notin" LabelSelector to perform an outer-join removal.
func (r *Manager) pruningResources(ctx context.Context, ns string, keys []string, obj client.Object) (err error) {
@@ -1,22 +0,0 @@
// Copyright 2020-2025 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package utils
import (
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)
func NamesMatchingPredicate(names ...string) builder.Predicates {
return builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool {
for _, name := range names {
if object.GetName() == name {
return true
}
}
return false
}))
}
+1
View File
@@ -4,5 +4,6 @@
package utils
type ControllerOptions struct {
ConfigurationName string
MaxConcurrentReconciles int
}
+45
View File
@@ -0,0 +1,45 @@
// Copyright 2020-2025 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package utils
import (
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
)
var CapsuleConfigSpecChangedPredicate = predicate.Funcs{
UpdateFunc: func(e event.UpdateEvent) bool {
oldObj, ok1 := e.ObjectOld.(*capsulev1beta2.CapsuleConfiguration)
newObj, ok2 := e.ObjectNew.(*capsulev1beta2.CapsuleConfiguration)
if !ok1 || !ok2 {
return false
}
if len(oldObj.Spec.Administrators) != len(newObj.Spec.Administrators) {
return true
}
return false
},
CreateFunc: func(e event.CreateEvent) bool { return false },
DeleteFunc: func(e event.DeleteEvent) bool { return false },
GenericFunc: func(e event.GenericEvent) bool { return false },
}
func NamesMatchingPredicate(names ...string) builder.Predicates {
return builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool {
for _, name := range names {
if object.GetName() == name {
return true
}
}
return false
}))
}