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
+15 -12
View File
@@ -93,7 +93,7 @@ func main() {
var enableLeaderElection, version bool
var metricsAddr, ns, configurationName string
var metricsAddr, ns string
var webhookPort int
@@ -106,7 +106,7 @@ func main() {
"Enable leader election for controller manager. "+
"Enabling this will ensure there is only one active controller manager.")
flag.BoolVar(&version, "version", false, "Print the Capsule version and exit")
flag.StringVar(&configurationName, "configuration-name", "default", "The CapsuleConfiguration resource name to use")
flag.StringVar(&controllerConfig.ConfigurationName, "configuration-name", "default", "The CapsuleConfiguration resource name to use")
opts := zap.Options{
EncoderConfigOptions: append([]zap.EncoderConfigOption{}, func(config *zapcore.EncoderConfig) {
@@ -126,12 +126,14 @@ func main() {
os.Exit(0)
}
setupLog.V(5).Info("Controller", "Options", controllerConfig)
if ns = os.Getenv("NAMESPACE"); len(ns) == 0 {
setupLog.Error(fmt.Errorf("unable to determinate the Namespace Capsule is running on"), "unable to start manager")
os.Exit(1)
}
if len(configurationName) == 0 {
if len(controllerConfig.ConfigurationName) == 0 {
setupLog.Error(fmt.Errorf("missing CapsuleConfiguration resource name"), "unable to start manager")
os.Exit(1)
}
@@ -163,7 +165,7 @@ func main() {
ctx := ctrl.SetupSignalHandler()
cfg := configuration.NewCapsuleConfiguration(ctx, manager.GetClient(), configurationName)
cfg := configuration.NewCapsuleConfiguration(ctx, manager.GetClient(), controllerConfig.ConfigurationName)
directClient, err := client.New(ctrl.GetConfigOrDie(), client.Options{
Scheme: manager.GetScheme(),
@@ -174,7 +176,7 @@ func main() {
os.Exit(1)
}
directCfg := configuration.NewCapsuleConfiguration(ctx, directClient, configurationName)
directCfg := configuration.NewCapsuleConfiguration(ctx, directClient, controllerConfig.ConfigurationName)
if directCfg.EnableTLSConfiguration() {
tlsReconciler := &tlscontroller.Reconciler{
@@ -203,11 +205,12 @@ func main() {
}
if err = (&tenantcontroller.Manager{
RESTConfig: manager.GetConfig(),
Client: manager.GetClient(),
Metrics: metrics.MustMakeTenantRecorder(),
Log: ctrl.Log.WithName("controllers").WithName("Tenant"),
Recorder: manager.GetEventRecorderFor("tenant-controller"),
RESTConfig: manager.GetConfig(),
Client: manager.GetClient(),
Metrics: metrics.MustMakeTenantRecorder(),
Log: ctrl.Log.WithName("controllers").WithName("Tenant"),
Recorder: manager.GetEventRecorderFor("tenant-controller"),
Configuration: cfg,
}).SetupWithManager(manager, controllerConfig); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Tenant")
os.Exit(1)
@@ -305,7 +308,7 @@ func main() {
os.Exit(1)
}
if err = rbacManager.SetupWithManager(ctx, manager, configurationName); err != nil {
if err = rbacManager.SetupWithManager(ctx, manager, controllerConfig); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Rbac")
os.Exit(1)
}
@@ -335,7 +338,7 @@ func main() {
if err = (&configcontroller.Manager{
Log: ctrl.Log.WithName("controllers").WithName("CapsuleConfiguration"),
}).SetupWithManager(manager, configurationName); err != nil {
}).SetupWithManager(manager, controllerConfig); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "CapsuleConfiguration")
os.Exit(1)
}
+8
View File
@@ -178,5 +178,13 @@ var _ = Describe("Administrators", Label("namespace", "permissions"), func() {
Expect(condition.Reason).To(Equal(meta.SucceededReason), "Expected namespace condition reason to be Succeeded")
})
By("deleting namespace", func() {
Expect(k8sClient.Delete(context.TODO(), ns2)).Should(Succeed())
})
By("deleting namespace", func() {
Expect(k8sClient.Delete(context.TODO(), ns1)).Should(Succeed())
})
})
})
+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
}))
}