Skip to content

Commit 4e871ec

Browse files
committed
Fix RoleBinding ownership conflict for multi-function namespaces
The shared deploy-function RoleBinding previously set a single Function as the controller owner, overwriting any prior owner. Deleting that Function would garbage-collect the RoleBinding, breaking other Functions in the same namespace. Use controllerutil.SetOwnerReference to append each Function as a non-controller owner so the RoleBinding is only cleaned up when all Functions in the namespace are deleted.
1 parent f51177a commit 4e871ec

3 files changed

Lines changed: 182 additions & 34 deletions

File tree

internal/controller/function_controller.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ import (
4848
)
4949

5050
const (
51-
deployFunctionRoleName = "func-operator-deploy-function"
52-
controllerConfigName = "func-operator-controller-config"
51+
deployFunctionRoleName = "func-operator-deploy-function"
52+
deployFunctionRoleBindingName = "deploy-function-default"
53+
controllerConfigName = "func-operator-controller-config"
5354

5455
funcAnnotationPrefix = "functions.knative.dev/"
5556
funcAnnotationLastDeployed = funcAnnotationPrefix + "last-deployed"

internal/controller/function_rbac.go

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import (
2626
apierrors "k8s.io/apimachinery/pkg/api/errors"
2727
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2828
"k8s.io/apimachinery/pkg/types"
29-
"k8s.io/utils/ptr"
29+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
3030
"sigs.k8s.io/controller-runtime/pkg/log"
3131
)
3232

@@ -109,37 +109,34 @@ func (r *FunctionReconciler) ensureDeployFunctionRole(ctx context.Context, names
109109
func (r *FunctionReconciler) ensureDeployFunctionRoleBinding(ctx context.Context, function *v1alpha1.Function) error {
110110
logger := log.FromContext(ctx)
111111

112-
expectedRoleBinding := &rbacv1.RoleBinding{
113-
ObjectMeta: metav1.ObjectMeta{
114-
Name: "deploy-function-default",
115-
Namespace: function.Namespace,
116-
OwnerReferences: []metav1.OwnerReference{
117-
{
118-
APIVersion: v1alpha1.GroupVersion.String(),
119-
Kind: "Function",
120-
Name: function.Name,
121-
UID: function.UID,
122-
Controller: ptr.To(true),
123-
},
124-
},
125-
},
126-
Subjects: []rbacv1.Subject{{
127-
Kind: "ServiceAccount",
128-
Name: "default",
129-
Namespace: function.Namespace,
130-
}},
131-
RoleRef: rbacv1.RoleRef{
132-
APIGroup: "rbac.authorization.k8s.io",
133-
Kind: "Role",
134-
Name: deployFunctionRoleName,
135-
},
112+
expectedSubjects := []rbacv1.Subject{{
113+
Kind: "ServiceAccount",
114+
Name: "default",
115+
Namespace: function.Namespace,
116+
}}
117+
118+
expectedRoleRef := rbacv1.RoleRef{
119+
APIGroup: "rbac.authorization.k8s.io",
120+
Kind: "Role",
121+
Name: deployFunctionRoleName,
136122
}
137123

138124
foundRoleBinding := &rbacv1.RoleBinding{}
139-
err := r.Get(ctx, types.NamespacedName{Name: expectedRoleBinding.Name, Namespace: expectedRoleBinding.Namespace}, foundRoleBinding)
125+
err := r.Get(ctx, types.NamespacedName{Name: deployFunctionRoleBindingName, Namespace: function.Namespace}, foundRoleBinding)
140126
if err != nil {
141127
if apierrors.IsNotFound(err) {
142-
if err := r.Create(ctx, expectedRoleBinding); err != nil {
128+
rb := &rbacv1.RoleBinding{
129+
ObjectMeta: metav1.ObjectMeta{
130+
Name: deployFunctionRoleBindingName,
131+
Namespace: function.Namespace,
132+
},
133+
Subjects: expectedSubjects,
134+
RoleRef: expectedRoleRef,
135+
}
136+
if err := controllerutil.SetOwnerReference(function, rb, r.Scheme); err != nil {
137+
return fmt.Errorf("failed to set owner reference: %w", err)
138+
}
139+
if err := r.Create(ctx, rb); err != nil {
143140
return fmt.Errorf("failed to create role binding: %w", err)
144141
}
145142
logger.Info("Created deploy-function role binding")
@@ -148,12 +145,30 @@ func (r *FunctionReconciler) ensureDeployFunctionRoleBinding(ctx context.Context
148145
return fmt.Errorf("failed to get role binding: %w", err)
149146
}
150147

151-
// Update if needed
152-
if !equality.Semantic.DeepDerivative(expectedRoleBinding, foundRoleBinding) {
153-
foundRoleBinding.Subjects = expectedRoleBinding.Subjects
154-
foundRoleBinding.RoleRef = expectedRoleBinding.RoleRef
155-
foundRoleBinding.OwnerReferences = expectedRoleBinding.OwnerReferences
148+
needsUpdate := false
149+
150+
hasRef, err := controllerutil.HasOwnerReference(foundRoleBinding.OwnerReferences, function, r.Scheme)
151+
if err != nil {
152+
return fmt.Errorf("failed to check owner reference: %w", err)
153+
}
154+
if !hasRef {
155+
if err := controllerutil.SetOwnerReference(function, foundRoleBinding, r.Scheme); err != nil {
156+
return fmt.Errorf("failed to set owner reference: %w", err)
157+
}
158+
needsUpdate = true
159+
}
160+
161+
if !equality.Semantic.DeepDerivative(expectedSubjects, foundRoleBinding.Subjects) {
162+
foundRoleBinding.Subjects = expectedSubjects
163+
needsUpdate = true
164+
}
165+
166+
if !equality.Semantic.DeepDerivative(expectedRoleRef, foundRoleBinding.RoleRef) {
167+
foundRoleBinding.RoleRef = expectedRoleRef
168+
needsUpdate = true
169+
}
156170

171+
if needsUpdate {
157172
if err := r.Update(ctx, foundRoleBinding); err != nil {
158173
return fmt.Errorf("failed to update role binding: %w", err)
159174
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package controller
2+
3+
import (
4+
functionsdevv1alpha1 "github.com/functions-dev/func-operator/api/v1alpha1"
5+
. "github.com/onsi/ginkgo/v2"
6+
. "github.com/onsi/gomega"
7+
v1 "k8s.io/api/core/v1"
8+
rbacv1 "k8s.io/api/rbac/v1"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"k8s.io/apimachinery/pkg/types"
11+
"k8s.io/apimachinery/pkg/util/rand"
12+
"k8s.io/client-go/tools/events"
13+
)
14+
15+
var _ = Describe("Function RBAC", func() {
16+
Context("ensureDeployFunctionRoleBinding", func() {
17+
var reconciler *FunctionReconciler
18+
var testNamespace string
19+
20+
BeforeEach(func() {
21+
testNamespace = "rbac-test-" + rand.String(6)
22+
ns := &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNamespace}}
23+
Expect(k8sClient.Create(ctx, ns)).To(Succeed())
24+
25+
reconciler = &FunctionReconciler{
26+
Client: k8sClient,
27+
Scheme: k8sClient.Scheme(),
28+
Recorder: &events.FakeRecorder{},
29+
}
30+
})
31+
32+
It("should add owner references for multiple Functions without overwriting", func() {
33+
functionA := &functionsdevv1alpha1.Function{
34+
ObjectMeta: metav1.ObjectMeta{
35+
Name: "function-a",
36+
Namespace: testNamespace,
37+
},
38+
Spec: functionsdevv1alpha1.FunctionSpec{
39+
Repository: functionsdevv1alpha1.FunctionSpecRepository{
40+
URL: "https://github.com/foo/bar",
41+
},
42+
},
43+
}
44+
Expect(k8sClient.Create(ctx, functionA)).To(Succeed())
45+
46+
functionB := &functionsdevv1alpha1.Function{
47+
ObjectMeta: metav1.ObjectMeta{
48+
Name: "function-b",
49+
Namespace: testNamespace,
50+
},
51+
Spec: functionsdevv1alpha1.FunctionSpec{
52+
Repository: functionsdevv1alpha1.FunctionSpecRepository{
53+
URL: "https://github.com/foo/baz",
54+
},
55+
},
56+
}
57+
Expect(k8sClient.Create(ctx, functionB)).To(Succeed())
58+
59+
By("Reconciling the RoleBinding for Function A")
60+
Expect(reconciler.ensureDeployFunctionRoleBinding(ctx, functionA)).To(Succeed())
61+
62+
By("Verifying RoleBinding was created with Function A as owner")
63+
rb := &rbacv1.RoleBinding{}
64+
Expect(k8sClient.Get(ctx, types.NamespacedName{
65+
Name: deployFunctionRoleBindingName,
66+
Namespace: testNamespace,
67+
}, rb)).To(Succeed())
68+
Expect(rb.OwnerReferences).To(HaveLen(1))
69+
Expect(rb.OwnerReferences[0].Name).To(Equal("function-a"))
70+
71+
By("Reconciling the RoleBinding for Function B")
72+
Expect(reconciler.ensureDeployFunctionRoleBinding(ctx, functionB)).To(Succeed())
73+
74+
By("Verifying RoleBinding now has both Functions as owners")
75+
Expect(k8sClient.Get(ctx, types.NamespacedName{
76+
Name: deployFunctionRoleBindingName,
77+
Namespace: testNamespace,
78+
}, rb)).To(Succeed())
79+
Expect(rb.OwnerReferences).To(HaveLen(2))
80+
81+
ownerNames := []string{rb.OwnerReferences[0].Name, rb.OwnerReferences[1].Name}
82+
Expect(ownerNames).To(ConsistOf("function-a", "function-b"))
83+
84+
By("Verifying no owner is marked as controller")
85+
for _, ref := range rb.OwnerReferences {
86+
if ref.Controller != nil {
87+
Expect(*ref.Controller).To(BeFalse())
88+
}
89+
}
90+
91+
By("Reconciling Function A again should be idempotent")
92+
Expect(reconciler.ensureDeployFunctionRoleBinding(ctx, functionA)).To(Succeed())
93+
94+
Expect(k8sClient.Get(ctx, types.NamespacedName{
95+
Name: deployFunctionRoleBindingName,
96+
Namespace: testNamespace,
97+
}, rb)).To(Succeed())
98+
Expect(rb.OwnerReferences).To(HaveLen(2))
99+
})
100+
101+
It("should set the expected Subjects and RoleRef", func() {
102+
function := &functionsdevv1alpha1.Function{
103+
ObjectMeta: metav1.ObjectMeta{
104+
Name: "function-rbac-check",
105+
Namespace: testNamespace,
106+
},
107+
Spec: functionsdevv1alpha1.FunctionSpec{
108+
Repository: functionsdevv1alpha1.FunctionSpecRepository{
109+
URL: "https://github.com/foo/bar",
110+
},
111+
},
112+
}
113+
Expect(k8sClient.Create(ctx, function)).To(Succeed())
114+
115+
Expect(reconciler.ensureDeployFunctionRoleBinding(ctx, function)).To(Succeed())
116+
117+
rb := &rbacv1.RoleBinding{}
118+
Expect(k8sClient.Get(ctx, types.NamespacedName{
119+
Name: deployFunctionRoleBindingName,
120+
Namespace: testNamespace,
121+
}, rb)).To(Succeed())
122+
123+
Expect(rb.Subjects).To(HaveLen(1))
124+
Expect(rb.Subjects[0].Kind).To(Equal("ServiceAccount"))
125+
Expect(rb.Subjects[0].Name).To(Equal("default"))
126+
127+
Expect(rb.RoleRef.APIGroup).To(Equal("rbac.authorization.k8s.io"))
128+
Expect(rb.RoleRef.Kind).To(Equal("Role"))
129+
Expect(rb.RoleRef.Name).To(Equal(deployFunctionRoleName))
130+
})
131+
})
132+
})

0 commit comments

Comments
 (0)