@@ -14,6 +14,7 @@ import (
14
14
15
15
appsv1 "k8s.io/api/apps/v1"
16
16
corev1 "k8s.io/api/core/v1"
17
+ rbacv1 "k8s.io/api/rbac/v1"
17
18
"k8s.io/apimachinery/pkg/api/errors"
18
19
"k8s.io/apimachinery/pkg/api/resource"
19
20
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -35,35 +36,44 @@ type MCPServerReconciler struct {
35
36
Scheme * runtime.Scheme
36
37
}
37
38
38
- // Allow the operator to manage MCPServer resources
39
- // +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=mcpservers,verbs=create;delete;get;list;patch;update;watch
40
- // +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=mcpservers/status,verbs=get;patch;update
41
- // +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=mcpservers/finalizers,verbs=update
42
-
43
- // Allow the operator to manage Deployments
44
- // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=create;delete;get;list;patch;update;watch
45
-
46
- // Allow the operator to manage Services
47
- // +kubebuilder:rbac:groups="",resources=services,verbs=create;delete;get;list;patch;update;watch
48
-
49
- // Allow the operator read manage Pods
50
- // +kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch
51
-
52
- // Allow the operator to manage ConfigMaps (including telemetry data)
53
- // +kubebuilder:rbac:groups="",resources=configmaps,verbs=create;delete;get;list;patch;update;watch
54
-
55
- // Allow the operator read manage Secrets
56
- // +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
39
+ // defaultRBACRules are the default RBAC rules that the
40
+ // ToolHive ProxyRunner and/or MCP server needs to have in order to run.
41
+ var defaultRBACRules = []rbacv1.PolicyRule {
42
+ {
43
+ APIGroups : []string {"apps" },
44
+ Resources : []string {"statefulsets" },
45
+ Verbs : []string {"get" , "list" , "watch" , "create" , "update" , "patch" , "delete" , "apply" },
46
+ },
47
+ {
48
+ APIGroups : []string {"" },
49
+ Resources : []string {"services" },
50
+ Verbs : []string {"get" , "list" , "watch" , "create" , "update" , "patch" , "delete" , "apply" },
51
+ },
52
+ {
53
+ APIGroups : []string {"" },
54
+ Resources : []string {"pods" },
55
+ Verbs : []string {"get" , "list" , "watch" },
56
+ },
57
+ {
58
+ APIGroups : []string {"" },
59
+ Resources : []string {"pods/log" },
60
+ Verbs : []string {"get" },
61
+ },
62
+ {
63
+ APIGroups : []string {"" },
64
+ Resources : []string {"pods/attach" },
65
+ Verbs : []string {"create" , "get" },
66
+ },
67
+ }
57
68
58
- // Allow the operator to manage events
59
- // +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
69
+ var ctxLogger = log .FromContext (context .Background ())
60
70
61
71
// Reconcile is part of the main kubernetes reconciliation loop which aims to
62
72
// move the current state of the cluster closer to the desired state.
63
73
//
64
74
//nolint:gocyclo
65
75
func (r * MCPServerReconciler ) Reconcile (ctx context.Context , req ctrl.Request ) (ctrl.Result , error ) {
66
- ctxLogger : = log .FromContext (ctx )
76
+ ctxLogger = log .FromContext (ctx )
67
77
68
78
// Fetch the MCPServer instance
69
79
mcpServer := & mcpv1alpha1.MCPServer {}
@@ -115,6 +125,12 @@ func (r *MCPServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
115
125
return ctrl.Result {}, err
116
126
}
117
127
128
+ // check if the RBAC resources are in place for the MCP server
129
+ if err := r .ensureRBACResources (ctx , mcpServer ); err != nil {
130
+ ctxLogger .Error (err , "Failed to ensure RBAC resources" )
131
+ return ctrl.Result {}, err
132
+ }
133
+
118
134
// Check if the deployment already exists, if not create a new one
119
135
deployment := & appsv1.Deployment {}
120
136
err = r .Get (ctx , types.NamespacedName {Name : mcpServer .Name , Namespace : mcpServer .Namespace }, deployment )
@@ -219,6 +235,131 @@ func (r *MCPServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
219
235
return ctrl.Result {}, nil
220
236
}
221
237
238
+ // ensureRBACResource is a generic helper function to ensure a Kubernetes resource exists and is up to date
239
+ func (r * MCPServerReconciler ) ensureRBACResource (
240
+ ctx context.Context ,
241
+ mcpServer * mcpv1alpha1.MCPServer ,
242
+ resourceType string ,
243
+ createResource func () client.Object ,
244
+ ) error {
245
+ current := createResource ()
246
+ objectKey := types.NamespacedName {Name : current .GetName (), Namespace : current .GetNamespace ()}
247
+ err := r .Get (ctx , objectKey , current )
248
+
249
+ if errors .IsNotFound (err ) {
250
+ return r .createRBACResource (ctx , mcpServer , resourceType , createResource )
251
+ } else if err != nil {
252
+ return fmt .Errorf ("failed to get %s: %w" , resourceType , err )
253
+ }
254
+
255
+ return r .updateRBACResourceIfNeeded (ctx , mcpServer , resourceType , createResource , current )
256
+ }
257
+
258
+ // createRBACResource creates a new RBAC resource
259
+ func (r * MCPServerReconciler ) createRBACResource (
260
+ ctx context.Context ,
261
+ mcpServer * mcpv1alpha1.MCPServer ,
262
+ resourceType string ,
263
+ createResource func () client.Object ,
264
+ ) error {
265
+ desired := createResource ()
266
+ if err := controllerutil .SetControllerReference (mcpServer , desired , r .Scheme ); err != nil {
267
+ logger .Error (fmt .Sprintf ("Failed to set controller reference for %s" , resourceType ), err )
268
+ return nil
269
+ }
270
+
271
+ ctxLogger .Info (
272
+ fmt .Sprintf ("%s does not exist, creating %s" , resourceType , resourceType ),
273
+ fmt .Sprintf ("%s.Name" , resourceType ),
274
+ desired .GetName (),
275
+ )
276
+ if err := r .Create (ctx , desired ); err != nil {
277
+ return fmt .Errorf ("failed to create %s: %w" , resourceType , err )
278
+ }
279
+ ctxLogger .Info (fmt .Sprintf ("%s created" , resourceType ), fmt .Sprintf ("%s.Name" , resourceType ), desired .GetName ())
280
+ return nil
281
+ }
282
+
283
+ // updateRBACResourceIfNeeded updates an RBAC resource if changes are detected
284
+ func (r * MCPServerReconciler ) updateRBACResourceIfNeeded (
285
+ ctx context.Context ,
286
+ mcpServer * mcpv1alpha1.MCPServer ,
287
+ resourceType string ,
288
+ createResource func () client.Object ,
289
+ current client.Object ,
290
+ ) error {
291
+ desired := createResource ()
292
+ if err := controllerutil .SetControllerReference (mcpServer , desired , r .Scheme ); err != nil {
293
+ logger .Error (fmt .Sprintf ("Failed to set controller reference for %s" , resourceType ), err )
294
+ return nil
295
+ }
296
+
297
+ if ! reflect .DeepEqual (current , desired ) {
298
+ ctxLogger .Info (
299
+ fmt .Sprintf ("%s exists, updating %s" , resourceType , resourceType ),
300
+ fmt .Sprintf ("%s.Name" , resourceType ),
301
+ desired .GetName (),
302
+ )
303
+ if err := r .Update (ctx , desired ); err != nil {
304
+ return fmt .Errorf ("failed to update %s: %w" , resourceType , err )
305
+ }
306
+ ctxLogger .Info (fmt .Sprintf ("%s updated" , resourceType ), fmt .Sprintf ("%s.Name" , resourceType ), desired .GetName ())
307
+ }
308
+ return nil
309
+ }
310
+
311
+ // ensureRBACResources ensures that the RBAC resources are in place for the MCP server
312
+ func (r * MCPServerReconciler ) ensureRBACResources (ctx context.Context , mcpServer * mcpv1alpha1.MCPServer ) error {
313
+ proxyRunnerNameForRBAC := fmt .Sprintf ("%s-proxy-runner" , mcpServer .Name )
314
+
315
+ // Ensure Role
316
+ if err := r .ensureRBACResource (ctx , mcpServer , "Role" , func () client.Object {
317
+ return & rbacv1.Role {
318
+ ObjectMeta : metav1.ObjectMeta {
319
+ Name : proxyRunnerNameForRBAC ,
320
+ Namespace : mcpServer .Namespace ,
321
+ },
322
+ Rules : defaultRBACRules ,
323
+ }
324
+ }); err != nil {
325
+ return err
326
+ }
327
+
328
+ // Ensure ServiceAccount
329
+ if err := r .ensureRBACResource (ctx , mcpServer , "ServiceAccount" , func () client.Object {
330
+ return & corev1.ServiceAccount {
331
+ ObjectMeta : metav1.ObjectMeta {
332
+ Name : proxyRunnerNameForRBAC ,
333
+ Namespace : mcpServer .Namespace ,
334
+ },
335
+ }
336
+ }); err != nil {
337
+ return err
338
+ }
339
+
340
+ // Ensure RoleBinding
341
+ return r .ensureRBACResource (ctx , mcpServer , "RoleBinding" , func () client.Object {
342
+ return & rbacv1.RoleBinding {
343
+ ObjectMeta : metav1.ObjectMeta {
344
+ Name : proxyRunnerNameForRBAC ,
345
+ Namespace : mcpServer .Namespace ,
346
+ },
347
+ RoleRef : rbacv1.RoleRef {
348
+ APIGroup : "rbac.authorization.k8s.io" ,
349
+ Kind : "Role" ,
350
+ Name : proxyRunnerNameForRBAC ,
351
+ },
352
+ Subjects : []rbacv1.Subject {
353
+ {
354
+ Kind : "ServiceAccount" ,
355
+ Name : proxyRunnerNameForRBAC ,
356
+ Namespace : mcpServer .Namespace ,
357
+ },
358
+ },
359
+ }
360
+ })
361
+ }
362
+
222
363
// deploymentForMCPServer returns a MCPServer Deployment object
223
364
//
224
365
//nolint:gocyclo
@@ -383,7 +524,7 @@ func (r *MCPServerReconciler) deploymentForMCPServer(m *mcpv1alpha1.MCPServer) *
383
524
Labels : ls , // Keep original labels for pod template
384
525
},
385
526
Spec : corev1.PodSpec {
386
- ServiceAccountName : "toolhive" ,
527
+ ServiceAccountName : fmt . Sprintf ( "%s-proxy-runner" , m . Name ) ,
387
528
Containers : []corev1.Container {{
388
529
Image : getToolhiveRunnerImage (),
389
530
Name : "toolhive" ,
0 commit comments