Skip to content

Commit 7e7d9d0

Browse files
feat(config): administrators get delete privileges for tenant namespaces (#1749)
Signed-off-by: Oliver Bähler <[email protected]>
1 parent 581a8fe commit 7e7d9d0

File tree

10 files changed

+137
-47
lines changed

10 files changed

+137
-47
lines changed

cmd/main.go

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ func main() {
9393

9494
var enableLeaderElection, version bool
9595

96-
var metricsAddr, ns, configurationName string
96+
var metricsAddr, ns string
9797

9898
var webhookPort int
9999

@@ -106,7 +106,7 @@ func main() {
106106
"Enable leader election for controller manager. "+
107107
"Enabling this will ensure there is only one active controller manager.")
108108
flag.BoolVar(&version, "version", false, "Print the Capsule version and exit")
109-
flag.StringVar(&configurationName, "configuration-name", "default", "The CapsuleConfiguration resource name to use")
109+
flag.StringVar(&controllerConfig.ConfigurationName, "configuration-name", "default", "The CapsuleConfiguration resource name to use")
110110

111111
opts := zap.Options{
112112
EncoderConfigOptions: append([]zap.EncoderConfigOption{}, func(config *zapcore.EncoderConfig) {
@@ -126,12 +126,14 @@ func main() {
126126
os.Exit(0)
127127
}
128128

129+
setupLog.V(5).Info("Controller", "Options", controllerConfig)
130+
129131
if ns = os.Getenv("NAMESPACE"); len(ns) == 0 {
130132
setupLog.Error(fmt.Errorf("unable to determinate the Namespace Capsule is running on"), "unable to start manager")
131133
os.Exit(1)
132134
}
133135

134-
if len(configurationName) == 0 {
136+
if len(controllerConfig.ConfigurationName) == 0 {
135137
setupLog.Error(fmt.Errorf("missing CapsuleConfiguration resource name"), "unable to start manager")
136138
os.Exit(1)
137139
}
@@ -163,7 +165,7 @@ func main() {
163165

164166
ctx := ctrl.SetupSignalHandler()
165167

166-
cfg := configuration.NewCapsuleConfiguration(ctx, manager.GetClient(), configurationName)
168+
cfg := configuration.NewCapsuleConfiguration(ctx, manager.GetClient(), controllerConfig.ConfigurationName)
167169

168170
directClient, err := client.New(ctrl.GetConfigOrDie(), client.Options{
169171
Scheme: manager.GetScheme(),
@@ -174,7 +176,7 @@ func main() {
174176
os.Exit(1)
175177
}
176178

177-
directCfg := configuration.NewCapsuleConfiguration(ctx, directClient, configurationName)
179+
directCfg := configuration.NewCapsuleConfiguration(ctx, directClient, controllerConfig.ConfigurationName)
178180

179181
if directCfg.EnableTLSConfiguration() {
180182
tlsReconciler := &tlscontroller.Reconciler{
@@ -203,11 +205,12 @@ func main() {
203205
}
204206

205207
if err = (&tenantcontroller.Manager{
206-
RESTConfig: manager.GetConfig(),
207-
Client: manager.GetClient(),
208-
Metrics: metrics.MustMakeTenantRecorder(),
209-
Log: ctrl.Log.WithName("controllers").WithName("Tenant"),
210-
Recorder: manager.GetEventRecorderFor("tenant-controller"),
208+
RESTConfig: manager.GetConfig(),
209+
Client: manager.GetClient(),
210+
Metrics: metrics.MustMakeTenantRecorder(),
211+
Log: ctrl.Log.WithName("controllers").WithName("Tenant"),
212+
Recorder: manager.GetEventRecorderFor("tenant-controller"),
213+
Configuration: cfg,
211214
}).SetupWithManager(manager, controllerConfig); err != nil {
212215
setupLog.Error(err, "unable to create controller", "controller", "Tenant")
213216
os.Exit(1)
@@ -305,7 +308,7 @@ func main() {
305308
os.Exit(1)
306309
}
307310

308-
if err = rbacManager.SetupWithManager(ctx, manager, configurationName); err != nil {
311+
if err = rbacManager.SetupWithManager(ctx, manager, controllerConfig); err != nil {
309312
setupLog.Error(err, "unable to create controller", "controller", "Rbac")
310313
os.Exit(1)
311314
}
@@ -335,7 +338,7 @@ func main() {
335338

336339
if err = (&configcontroller.Manager{
337340
Log: ctrl.Log.WithName("controllers").WithName("CapsuleConfiguration"),
338-
}).SetupWithManager(manager, configurationName); err != nil {
341+
}).SetupWithManager(manager, controllerConfig); err != nil {
339342
setupLog.Error(err, "unable to create controller", "controller", "CapsuleConfiguration")
340343
os.Exit(1)
341344
}

e2e/administrators_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,5 +178,13 @@ var _ = Describe("Administrators", Label("namespace", "permissions"), func() {
178178
Expect(condition.Reason).To(Equal(meta.SucceededReason), "Expected namespace condition reason to be Succeeded")
179179
})
180180

181+
By("deleting namespace", func() {
182+
Expect(k8sClient.Delete(context.TODO(), ns2)).Should(Succeed())
183+
})
184+
185+
By("deleting namespace", func() {
186+
Expect(k8sClient.Delete(context.TODO(), ns1)).Should(Succeed())
187+
})
188+
181189
})
182190
})

internal/controllers/cfg/manager.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ type Manager struct {
2323
Log logr.Logger
2424
}
2525

26-
func (c *Manager) SetupWithManager(mgr ctrl.Manager, configurationName string) error {
26+
func (c *Manager) SetupWithManager(mgr ctrl.Manager, ctrlConfig utils.ControllerOptions) error {
2727
c.client = mgr.GetClient()
2828

2929
return ctrl.NewControllerManagedBy(mgr).
30-
For(&capsulev1beta2.CapsuleConfiguration{}, utils.NamesMatchingPredicate(configurationName)).
30+
For(&capsulev1beta2.CapsuleConfiguration{}, utils.NamesMatchingPredicate(ctrlConfig.ConfigurationName)).
3131
Complete(c)
3232
}
3333

internal/controllers/rbac/manager.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ type Manager struct {
3737
}
3838

3939
//nolint:revive
40-
func (r *Manager) SetupWithManager(ctx context.Context, mgr ctrl.Manager, configurationName string) (err error) {
40+
func (r *Manager) SetupWithManager(ctx context.Context, mgr ctrl.Manager, ctrlConfig utils.ControllerOptions) (err error) {
4141
namesPredicate := utils.NamesMatchingPredicate(ProvisionerRoleName, DeleterRoleName)
4242

4343
crErr := ctrl.NewControllerManagedBy(mgr).
@@ -51,7 +51,7 @@ func (r *Manager) SetupWithManager(ctx context.Context, mgr ctrl.Manager, config
5151
For(&rbacv1.ClusterRoleBinding{}, namesPredicate).
5252
Watches(&capsulev1beta2.CapsuleConfiguration{}, handler.Funcs{
5353
UpdateFunc: func(ctx context.Context, updateEvent event.TypedUpdateEvent[client.Object], limitingInterface workqueue.TypedRateLimitingInterface[reconcile.Request]) {
54-
if updateEvent.ObjectNew.GetName() == configurationName {
54+
if updateEvent.ObjectNew.GetName() == ctrlConfig.ConfigurationName {
5555
if crbErr := r.EnsureClusterRoleBindingsProvisioner(ctx); crbErr != nil {
5656
r.Log.Error(err, "cannot update ClusterRoleBinding upon CapsuleConfiguration update")
5757
}

internal/controllers/tenant/manager.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"k8s.io/client-go/tools/record"
1919
"k8s.io/client-go/util/retry"
2020
ctrl "sigs.k8s.io/controller-runtime"
21+
"sigs.k8s.io/controller-runtime/pkg/builder"
2122
"sigs.k8s.io/controller-runtime/pkg/client"
2223
"sigs.k8s.io/controller-runtime/pkg/controller"
2324
"sigs.k8s.io/controller-runtime/pkg/handler"
@@ -27,26 +28,34 @@ import (
2728
"github.com/projectcapsule/capsule/internal/controllers/utils"
2829
"github.com/projectcapsule/capsule/internal/metrics"
2930
meta "github.com/projectcapsule/capsule/pkg/api/meta"
31+
"github.com/projectcapsule/capsule/pkg/configuration"
3032
)
3133

3234
type Manager struct {
3335
client.Client
3436

35-
Metrics *metrics.TenantRecorder
36-
Log logr.Logger
37-
Recorder record.EventRecorder
38-
RESTConfig *rest.Config
37+
Metrics *metrics.TenantRecorder
38+
Log logr.Logger
39+
Recorder record.EventRecorder
40+
Configuration configuration.Configuration
41+
RESTConfig *rest.Config
3942
}
4043

41-
func (r *Manager) SetupWithManager(mgr ctrl.Manager, cfg utils.ControllerOptions) error {
44+
func (r *Manager) SetupWithManager(mgr ctrl.Manager, ctrlConfig utils.ControllerOptions) error {
4245
return ctrl.NewControllerManagedBy(mgr).
4346
For(&capsulev1beta2.Tenant{}).
4447
Owns(&networkingv1.NetworkPolicy{}).
4548
Owns(&corev1.LimitRange{}).
4649
Owns(&corev1.ResourceQuota{}).
4750
Owns(&rbacv1.RoleBinding{}).
51+
Watches(
52+
&capsulev1beta2.CapsuleConfiguration{},
53+
handler.EnqueueRequestsFromMapFunc(r.enqueueAllTenants),
54+
utils.NamesMatchingPredicate(ctrlConfig.ConfigurationName),
55+
builder.WithPredicates(utils.CapsuleConfigSpecChangedPredicate),
56+
).
4857
Watches(&corev1.Namespace{}, handler.EnqueueRequestForOwner(mgr.GetScheme(), mgr.GetRESTMapper(), &capsulev1beta2.Tenant{})).
49-
WithOptions(controller.Options{MaxConcurrentReconciles: cfg.MaxConcurrentReconciles}).
58+
WithOptions(controller.Options{MaxConcurrentReconciles: ctrlConfig.MaxConcurrentReconciles}).
5059
Complete(r)
5160
}
5261

internal/controllers/tenant/rolebindings.go

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,28 @@ import (
1515
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
1616

1717
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
18+
"github.com/projectcapsule/capsule/internal/controllers/rbac"
1819
"github.com/projectcapsule/capsule/pkg/api"
1920
"github.com/projectcapsule/capsule/pkg/api/meta"
2021
)
2122

2223
// ownerClusterRoleBindings generates a Capsule AdditionalRoleBinding object for the Owner dynamic clusterrole in order
2324
// to take advantage of the additional role binding feature.
2425
func (r *Manager) ownerClusterRoleBindings(owner api.OwnerSpec, clusterRole string) api.AdditionalRoleBindingsSpec {
26+
rb := r.userClusterRoleBindings(owner.UserSpec, clusterRole)
27+
28+
if owner.Labels != nil {
29+
rb.Labels = owner.Labels
30+
}
31+
32+
if owner.Annotations != nil {
33+
rb.Labels = owner.Annotations
34+
}
35+
36+
return rb
37+
}
38+
39+
func (r *Manager) userClusterRoleBindings(owner api.UserSpec, clusterRole string) api.AdditionalRoleBindingsSpec {
2540
var subject rbacv1.Subject
2641

2742
if owner.Kind == "ServiceAccount" {
@@ -45,8 +60,6 @@ func (r *Manager) ownerClusterRoleBindings(owner api.OwnerSpec, clusterRole stri
4560
Subjects: []rbacv1.Subject{
4661
subject,
4762
},
48-
Labels: owner.Labels,
49-
Annotations: owner.Annotations,
5063
}
5164
}
5265

@@ -80,6 +93,12 @@ func (r *Manager) syncRoleBindings(ctx context.Context, tenant *capsulev1beta2.T
8093
keys = append(keys, hashFn(i))
8194
}
8295

96+
for _, i := range r.Configuration.Administrators() {
97+
cr := r.userClusterRoleBindings(i, rbac.DeleterRoleName)
98+
99+
keys = append(keys, hashFn(cr))
100+
}
101+
83102
group := new(errgroup.Group)
84103

85104
for _, ns := range tenant.Status.Namespaces {
@@ -98,14 +117,18 @@ func (r *Manager) syncAdditionalRoleBinding(ctx context.Context, tenant *capsule
98117
return err
99118
}
100119

101-
var roleBindings []api.AdditionalRoleBindingsSpec
120+
roleBindings := make([]api.AdditionalRoleBindingsSpec, 0)
102121

103122
for _, owner := range tenant.Spec.Owners {
104123
for _, clusterRoleName := range owner.ClusterRoles {
105124
roleBindings = append(roleBindings, r.ownerClusterRoleBindings(owner, clusterRoleName))
106125
}
107126
}
108127

128+
for _, a := range r.Configuration.Administrators() {
129+
roleBindings = append(roleBindings, r.userClusterRoleBindings(a, rbac.DeleterRoleName))
130+
}
131+
109132
roleBindings = append(roleBindings, tenant.Spec.AdditionalRoleBindings...)
110133

111134
for i, roleBinding := range roleBindings {

internal/controllers/tenant/utils.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,36 @@ import (
1010
"k8s.io/apimachinery/pkg/labels"
1111
"k8s.io/apimachinery/pkg/runtime"
1212
"k8s.io/apimachinery/pkg/selection"
13+
"k8s.io/apimachinery/pkg/types"
1314
"k8s.io/client-go/util/retry"
1415
"sigs.k8s.io/controller-runtime/pkg/client"
1516
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
17+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
1618

19+
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
1720
"github.com/projectcapsule/capsule/pkg/utils"
1821
)
1922

23+
func (r *Manager) enqueueAllTenants(ctx context.Context, _ client.Object) []reconcile.Request {
24+
var tenants capsulev1beta2.TenantList
25+
if err := r.List(ctx, &tenants); err != nil {
26+
r.Log.Error(err, "failed to list Tenants for class event")
27+
28+
return nil
29+
}
30+
31+
reqs := make([]reconcile.Request, 0, len(tenants.Items))
32+
for i := range tenants.Items {
33+
reqs = append(reqs, reconcile.Request{
34+
NamespacedName: types.NamespacedName{
35+
Name: tenants.Items[i].Name,
36+
},
37+
})
38+
}
39+
40+
return reqs
41+
}
42+
2043
// pruningResources is taking care of removing the no more requested sub-resources as LimitRange, ResourceQuota or
2144
// NetworkPolicy using the "exists" and "notin" LabelSelector to perform an outer-join removal.
2245
func (r *Manager) pruningResources(ctx context.Context, ns string, keys []string, obj client.Object) (err error) {

internal/controllers/utils/name_matching.go

Lines changed: 0 additions & 22 deletions
This file was deleted.

internal/controllers/utils/options.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
package utils
55

66
type ControllerOptions struct {
7+
ConfigurationName string
78
MaxConcurrentReconciles int
89
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright 2020-2025 Project Capsule Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package utils
5+
6+
import (
7+
"sigs.k8s.io/controller-runtime/pkg/builder"
8+
"sigs.k8s.io/controller-runtime/pkg/client"
9+
"sigs.k8s.io/controller-runtime/pkg/event"
10+
"sigs.k8s.io/controller-runtime/pkg/predicate"
11+
12+
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
13+
)
14+
15+
var CapsuleConfigSpecChangedPredicate = predicate.Funcs{
16+
UpdateFunc: func(e event.UpdateEvent) bool {
17+
oldObj, ok1 := e.ObjectOld.(*capsulev1beta2.CapsuleConfiguration)
18+
newObj, ok2 := e.ObjectNew.(*capsulev1beta2.CapsuleConfiguration)
19+
if !ok1 || !ok2 {
20+
return false
21+
}
22+
23+
if len(oldObj.Spec.Administrators) != len(newObj.Spec.Administrators) {
24+
return true
25+
}
26+
27+
return false
28+
},
29+
30+
CreateFunc: func(e event.CreateEvent) bool { return false },
31+
DeleteFunc: func(e event.DeleteEvent) bool { return false },
32+
GenericFunc: func(e event.GenericEvent) bool { return false },
33+
}
34+
35+
func NamesMatchingPredicate(names ...string) builder.Predicates {
36+
return builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool {
37+
for _, name := range names {
38+
if object.GetName() == name {
39+
return true
40+
}
41+
}
42+
43+
return false
44+
}))
45+
}

0 commit comments

Comments
 (0)