Skip to content

Commit 8409a77

Browse files
gman0olamilekan000
authored andcommitted
Reworked RBAC machinery for APIServiceExports (#451)
* Reworked RBAC machinery for APIServiceExports Added new APIServiceExportRBAC controller for managing APIServiceExport RBACs. Additionally: * kube-binder ClusterRoleBinding creation/update was moved to /bind handler * Redundant boundschemas and boundschemas/status rules in `kube-binder-<ClusterBinding.Namespace>` ClusterRole were removed, because they are already present in `kube-binder` ClusterRole. * e2e: adjusted tests for the new APIServiceExport RBAC machinery * cleanup manager * serviceexportrbac reconciler: don't set Controller=True in owner refs * serviceexportrbac reconciler unit test: add missing t.Helper() * code cleaning * added docs into developers/architecture.md On-behalf-of: @SAP robert.vasek@sap.com Signed-off-by: Robert Vasek <robert.vasek@clyso.com>
1 parent a4c2242 commit 8409a77

11 files changed

Lines changed: 1693 additions & 694 deletions

File tree

backend/controllers/clusterbinding/clusterbinding_controller.go

Lines changed: 0 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,9 @@ import (
2121
"fmt"
2222
"reflect"
2323

24-
v1 "k8s.io/api/core/v1"
25-
rbacv1 "k8s.io/api/rbac/v1"
2624
"k8s.io/apimachinery/pkg/api/errors"
27-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2825
"k8s.io/apimachinery/pkg/types"
2926
ctrl "sigs.k8s.io/controller-runtime"
30-
"sigs.k8s.io/controller-runtime/pkg/cache"
3127
"sigs.k8s.io/controller-runtime/pkg/client"
3228
"sigs.k8s.io/controller-runtime/pkg/cluster"
3329
"sigs.k8s.io/controller-runtime/pkg/controller"
@@ -64,84 +60,6 @@ func NewClusterBindingReconciler(
6460
manager: mgr,
6561
opts: opts,
6662
scope: scope,
67-
reconciler: reconciler{
68-
scope: scope,
69-
listServiceExports: func(ctx context.Context, cache cache.Cache, ns string) ([]*kubebindv1alpha2.APIServiceExport, error) {
70-
var list kubebindv1alpha2.APIServiceExportList
71-
if err := cache.List(ctx, &list, client.InNamespace(ns)); err != nil {
72-
return nil, err
73-
}
74-
var exports []*kubebindv1alpha2.APIServiceExport
75-
for i := range list.Items {
76-
exports = append(exports, &list.Items[i])
77-
}
78-
return exports, nil
79-
},
80-
getBoundSchema: func(ctx context.Context, cache cache.Cache, namespace, name string) (*kubebindv1alpha2.BoundSchema, error) {
81-
result := &kubebindv1alpha2.BoundSchema{}
82-
err := cache.Get(ctx, types.NamespacedName{Namespace: namespace, Name: name}, result)
83-
if err != nil {
84-
return nil, fmt.Errorf("failed to get BoundSchema %q: %w", name, err)
85-
}
86-
return result, nil
87-
},
88-
getClusterRole: func(ctx context.Context, cache cache.Cache, name string) (*rbacv1.ClusterRole, error) {
89-
var role rbacv1.ClusterRole
90-
key := types.NamespacedName{Name: name}
91-
if err := cache.Get(ctx, key, &role); err != nil {
92-
return nil, err
93-
}
94-
return &role, nil
95-
},
96-
createClusterRole: func(ctx context.Context, client client.Client, binding *rbacv1.ClusterRole) error {
97-
return client.Create(ctx, binding)
98-
},
99-
updateClusterRole: func(ctx context.Context, client client.Client, binding *rbacv1.ClusterRole) error {
100-
return client.Update(ctx, binding)
101-
},
102-
getClusterRoleBinding: func(ctx context.Context, cache cache.Cache, name string) (*rbacv1.ClusterRoleBinding, error) {
103-
var binding rbacv1.ClusterRoleBinding
104-
key := types.NamespacedName{Name: name}
105-
if err := cache.Get(ctx, key, &binding); err != nil {
106-
return nil, err
107-
}
108-
return &binding, nil
109-
},
110-
createClusterRoleBinding: func(ctx context.Context, client client.Client, binding *rbacv1.ClusterRoleBinding) error {
111-
return client.Create(ctx, binding)
112-
},
113-
updateClusterRoleBinding: func(ctx context.Context, client client.Client, binding *rbacv1.ClusterRoleBinding) error {
114-
return client.Update(ctx, binding)
115-
},
116-
deleteClusterRoleBinding: func(ctx context.Context, client client.Client, name string) error {
117-
binding := &rbacv1.ClusterRoleBinding{
118-
ObjectMeta: metav1.ObjectMeta{Name: name},
119-
}
120-
return client.Delete(ctx, binding)
121-
},
122-
getNamespace: func(ctx context.Context, cache cache.Cache, name string) (*v1.Namespace, error) {
123-
var ns v1.Namespace
124-
key := types.NamespacedName{Name: name}
125-
if err := cache.Get(ctx, key, &ns); err != nil {
126-
return nil, err
127-
}
128-
return &ns, nil
129-
},
130-
createRoleBinding: func(ctx context.Context, client client.Client, binding *rbacv1.RoleBinding) error {
131-
return client.Create(ctx, binding)
132-
},
133-
updateRoleBinding: func(ctx context.Context, client client.Client, binding *rbacv1.RoleBinding) error {
134-
return client.Update(ctx, binding)
135-
},
136-
getRoleBinding: func(ctx context.Context, cache cache.Cache, ns, name string) (*rbacv1.RoleBinding, error) {
137-
var binding rbacv1.RoleBinding
138-
key := types.NamespacedName{Namespace: ns, Name: name}
139-
if err := cache.Get(ctx, key, &binding); err != nil {
140-
return nil, err
141-
}
142-
return &binding, nil
143-
},
144-
},
14563
}
14664

14765
return r, nil
@@ -153,10 +71,6 @@ func NewClusterBindingReconciler(
15371
//+kubebuilder:rbac:groups=kube-bind.io,resources=apiserviceexports,verbs=get;list;watch
15472
//+kubebuilder:rbac:groups=kube-bind.io,resources=collections,verbs=get;list;watch
15573
//+kubebuilder:rbac:groups=kube-bind.io,resources=apiserviceexporttemplates,verbs=get;list;watch
156-
//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterroles,verbs=get;list;watch;create;update;patch;delete
157-
//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterrolebindings,verbs=get;list;watch;create;update;patch;delete
158-
//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,verbs=get;list;watch;create;update;patch;delete
159-
//+kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch
16074

16175
// Reconcile is part of the main kubernetes reconciliation loop which aims to
16276
// move the current state of the cluster closer to the desired state.
@@ -210,9 +124,6 @@ func (r *ClusterBindingReconciler) Reconcile(ctx context.Context, req mcreconcil
210124
func (r *ClusterBindingReconciler) SetupWithManager(mgr mcmanager.Manager) error {
211125
return mcbuilder.ControllerManagedBy(mgr).
212126
For(&kubebindv1alpha2.ClusterBinding{}).
213-
Owns(&rbacv1.ClusterRole{}).
214-
Owns(&rbacv1.ClusterRoleBinding{}).
215-
Owns(&rbacv1.RoleBinding{}).
216127
Watches(
217128
&kubebindv1alpha2.APIServiceExport{},
218129
mapAPIResourceSchema,

backend/controllers/clusterbinding/clusterbinding_reconcile.go

Lines changed: 1 addition & 223 deletions
Original file line numberDiff line numberDiff line change
@@ -18,63 +18,25 @@ package clusterbinding
1818

1919
import (
2020
"context"
21-
"fmt"
22-
"maps"
23-
"reflect"
24-
"slices"
2521
"time"
2622

27-
corev1 "k8s.io/api/core/v1"
28-
rbacv1 "k8s.io/api/rbac/v1"
29-
"k8s.io/apimachinery/pkg/api/errors"
30-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3123
utilerrors "k8s.io/apimachinery/pkg/util/errors"
32-
"k8s.io/utils/ptr"
3324
"sigs.k8s.io/controller-runtime/pkg/cache"
3425
"sigs.k8s.io/controller-runtime/pkg/client"
3526

36-
kuberesources "github.com/kube-bind/kube-bind/backend/kubernetes/resources"
3727
kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2"
3828
conditionsapi "github.com/kube-bind/kube-bind/sdk/apis/third_party/conditions/apis/conditions/v1alpha1"
3929
"github.com/kube-bind/kube-bind/sdk/apis/third_party/conditions/util/conditions"
4030
)
4131

42-
type reconciler struct {
43-
scope kubebindv1alpha2.InformerScope
44-
45-
listServiceExports func(ctx context.Context, cache cache.Cache, ns string) ([]*kubebindv1alpha2.APIServiceExport, error)
46-
getBoundSchema func(ctx context.Context, cache cache.Cache, namespace, name string) (*kubebindv1alpha2.BoundSchema, error)
47-
getClusterRole func(ctx context.Context, cache cache.Cache, name string) (*rbacv1.ClusterRole, error)
48-
createClusterRole func(ctx context.Context, client client.Client, binding *rbacv1.ClusterRole) error
49-
updateClusterRole func(ctx context.Context, client client.Client, binding *rbacv1.ClusterRole) error
50-
51-
getClusterRoleBinding func(ctx context.Context, cache cache.Cache, name string) (*rbacv1.ClusterRoleBinding, error)
52-
createClusterRoleBinding func(ctx context.Context, client client.Client, binding *rbacv1.ClusterRoleBinding) error
53-
updateClusterRoleBinding func(ctx context.Context, client client.Client, binding *rbacv1.ClusterRoleBinding) error
54-
deleteClusterRoleBinding func(ctx context.Context, client client.Client, name string) error
55-
56-
getRoleBinding func(ctx context.Context, cache cache.Cache, ns, name string) (*rbacv1.RoleBinding, error)
57-
createRoleBinding func(ctx context.Context, client client.Client, binding *rbacv1.RoleBinding) error
58-
updateRoleBinding func(ctx context.Context, client client.Client, binding *rbacv1.RoleBinding) error
59-
60-
getNamespace func(ctx context.Context, cache cache.Cache, name string) (*corev1.Namespace, error)
61-
}
32+
type reconciler struct{}
6233

6334
func (r *reconciler) reconcile(ctx context.Context, client client.Client, cache cache.Cache, clusterBinding *kubebindv1alpha2.ClusterBinding) error {
6435
var errs []error
6536

6637
if err := r.ensureClusterBindingConditions(ctx, clusterBinding); err != nil {
6738
errs = append(errs, err)
6839
}
69-
if err := r.ensureRBACRoleBinding(ctx, client, cache, clusterBinding); err != nil {
70-
errs = append(errs, err)
71-
}
72-
if err := r.ensureRBACClusterRole(ctx, client, cache, clusterBinding); err != nil {
73-
errs = append(errs, err)
74-
}
75-
if err := r.ensureRBACClusterRoleBinding(ctx, client, cache, clusterBinding); err != nil {
76-
errs = append(errs, err)
77-
}
7840

7941
conditions.SetSummary(clusterBinding)
8042

@@ -120,187 +82,3 @@ func (r *reconciler) ensureClusterBindingConditions(_ context.Context, clusterBi
12082

12183
return nil
12284
}
123-
124-
func (r *reconciler) ensureRBACClusterRole(ctx context.Context, client client.Client, cache cache.Cache, clusterBinding *kubebindv1alpha2.ClusterBinding) error {
125-
name := "kube-binder-" + clusterBinding.Namespace
126-
role, err := r.getClusterRole(ctx, cache, name)
127-
if err != nil && !errors.IsNotFound(err) {
128-
return fmt.Errorf("failed to get ClusterRole %s: %w", name, err)
129-
}
130-
131-
ns, err := r.getNamespace(ctx, cache, clusterBinding.Namespace)
132-
if err != nil {
133-
return fmt.Errorf("failed to get Namespace %s: %w", clusterBinding.Namespace, err)
134-
}
135-
136-
exports, err := r.listServiceExports(ctx, cache, clusterBinding.Namespace)
137-
if err != nil {
138-
return fmt.Errorf("failed to list APIServiceExports: %w", err)
139-
}
140-
expected := &rbacv1.ClusterRole{
141-
ObjectMeta: metav1.ObjectMeta{
142-
Name: name,
143-
OwnerReferences: []metav1.OwnerReference{
144-
{
145-
APIVersion: "v1",
146-
Kind: "Namespace",
147-
Name: clusterBinding.Namespace,
148-
Controller: ptr.To(true),
149-
UID: ns.UID,
150-
},
151-
},
152-
},
153-
Rules: []rbacv1.PolicyRule{
154-
// Always need to be able to get/list/watch the BoundSchemas
155-
// to be able to figure out what to bind.
156-
{
157-
APIGroups: []string{kubebindv1alpha2.GroupName},
158-
Resources: []string{"boundschemas"},
159-
Verbs: []string{"get", "list", "watch"},
160-
},
161-
{
162-
APIGroups: []string{kubebindv1alpha2.GroupName},
163-
Resources: []string{"boundschemas/status"},
164-
Verbs: []string{"get", "update", "patch"},
165-
},
166-
}}
167-
for _, export := range exports {
168-
// Collect unique GroupResources and sort for stable rule ordering.
169-
grSet := map[string]kubebindv1alpha2.GroupResource{}
170-
for _, res := range export.Spec.Resources {
171-
key := res.ResourceGroupName()
172-
grSet[key] = kubebindv1alpha2.GroupResource{Group: res.Group, Resource: res.Resource}
173-
}
174-
keys := slices.Collect(maps.Keys(grSet))
175-
slices.Sort(keys)
176-
for _, k := range keys {
177-
// k is already normalized (e.g., "pods.core" for empty group).
178-
schema, err := r.getBoundSchema(ctx, cache, clusterBinding.Namespace, k)
179-
if err != nil {
180-
return fmt.Errorf("failed to get BoundSchema %q: %w", k, err)
181-
}
182-
expected.Rules = append(expected.Rules,
183-
rbacv1.PolicyRule{
184-
APIGroups: []string{schema.Spec.Group},
185-
Resources: []string{schema.Spec.Names.Plural},
186-
Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"},
187-
},
188-
)
189-
}
190-
}
191-
192-
if role == nil {
193-
if err := r.createClusterRole(ctx, client, expected); err != nil {
194-
return fmt.Errorf("failed to create ClusterRole %s: %w", expected.Name, err)
195-
}
196-
} else if !reflect.DeepEqual(role.Rules, expected.Rules) {
197-
role = role.DeepCopy()
198-
role.Rules = expected.Rules
199-
if err := r.updateClusterRole(ctx, client, role); err != nil {
200-
return fmt.Errorf("failed to create ClusterRole %s: %w", role.Name, err)
201-
}
202-
}
203-
204-
return nil
205-
}
206-
207-
func (r *reconciler) ensureRBACClusterRoleBinding(ctx context.Context, client client.Client, cache cache.Cache, clusterBinding *kubebindv1alpha2.ClusterBinding) error {
208-
name := "kube-binder-" + clusterBinding.Namespace
209-
binding, err := r.getClusterRoleBinding(ctx, cache, name)
210-
if err != nil && !errors.IsNotFound(err) {
211-
return fmt.Errorf("failed to get ClusterRoleBinding %s: %w", name, err)
212-
}
213-
if r.scope != kubebindv1alpha2.ClusterScope {
214-
if err := r.deleteClusterRoleBinding(ctx, client, name); err != nil && !errors.IsNotFound(err) {
215-
return fmt.Errorf("failed to delete ClusterRoleBinding %s: %w", name, err)
216-
}
217-
}
218-
219-
ns, err := r.getNamespace(ctx, cache, clusterBinding.Namespace)
220-
if err != nil {
221-
return fmt.Errorf("failed to get Namespace %s: %w", clusterBinding.Namespace, err)
222-
}
223-
224-
expected := &rbacv1.ClusterRoleBinding{
225-
ObjectMeta: metav1.ObjectMeta{
226-
Name: name,
227-
OwnerReferences: []metav1.OwnerReference{
228-
{
229-
APIVersion: "v1",
230-
Kind: "Namespace",
231-
Name: clusterBinding.Namespace,
232-
Controller: ptr.To(true),
233-
UID: ns.UID,
234-
},
235-
},
236-
},
237-
Subjects: []rbacv1.Subject{
238-
{
239-
Kind: "ServiceAccount",
240-
Namespace: clusterBinding.Namespace,
241-
Name: kuberesources.ServiceAccountName,
242-
},
243-
},
244-
RoleRef: rbacv1.RoleRef{
245-
Kind: "ClusterRole",
246-
Name: name,
247-
APIGroup: "rbac.authorization.k8s.io",
248-
},
249-
}
250-
251-
if binding == nil {
252-
if err := r.createClusterRoleBinding(ctx, client, expected); err != nil {
253-
return fmt.Errorf("failed to create ClusterRoleBinding %s: %w", expected.Name, err)
254-
}
255-
} else if !reflect.DeepEqual(binding.Subjects, expected.Subjects) {
256-
binding = binding.DeepCopy()
257-
binding.Subjects = expected.Subjects
258-
// roleRef is immutable
259-
if err := r.updateClusterRoleBinding(ctx, client, binding); err != nil {
260-
return fmt.Errorf("failed to create ClusterRoleBinding %s: %w", expected.Namespace, err)
261-
}
262-
}
263-
264-
return nil
265-
}
266-
267-
func (r *reconciler) ensureRBACRoleBinding(ctx context.Context, client client.Client, cache cache.Cache, clusterBinding *kubebindv1alpha2.ClusterBinding) error {
268-
binding, err := r.getRoleBinding(ctx, cache, clusterBinding.Namespace, "kube-binder")
269-
if err != nil && !errors.IsNotFound(err) {
270-
return fmt.Errorf("failed to get RoleBinding \"kube-binder\": %w", err)
271-
}
272-
273-
expected := &rbacv1.RoleBinding{
274-
ObjectMeta: metav1.ObjectMeta{
275-
Name: kuberesources.ServiceAccountName,
276-
},
277-
Subjects: []rbacv1.Subject{
278-
{
279-
Kind: "ServiceAccount",
280-
Name: kuberesources.ServiceAccountName,
281-
Namespace: clusterBinding.Namespace,
282-
},
283-
},
284-
RoleRef: rbacv1.RoleRef{
285-
APIGroup: "rbac.authorization.k8s.io",
286-
Kind: "ClusterRole",
287-
Name: "kube-binder",
288-
},
289-
}
290-
291-
if binding == nil {
292-
expected.Namespace = clusterBinding.Namespace
293-
if err := r.createRoleBinding(ctx, client, expected); err != nil {
294-
return fmt.Errorf("failed to create RoleBinding %s: %w", expected.Name, err)
295-
}
296-
} else if !reflect.DeepEqual(binding.Subjects, expected.Subjects) {
297-
binding = binding.DeepCopy()
298-
binding.Subjects = expected.Subjects
299-
// roleRef is immutable
300-
if err := r.updateRoleBinding(ctx, client, binding); err != nil {
301-
return fmt.Errorf("failed to create RoleBinding %s: %w", expected.Namespace, err)
302-
}
303-
}
304-
305-
return nil
306-
}

0 commit comments

Comments
 (0)