Skip to content

Commit cf461c8

Browse files
authored
Multi-Namespace Support for Creating MCPServer's with ToolHive Operator (#620)
Signed-off-by: ChrisJBurns <29541485+ChrisJBurns@users.noreply.github.com>
1 parent 9f52ce7 commit cf461c8

File tree

4 files changed

+527
-32
lines changed

4 files changed

+527
-32
lines changed

cmd/thv-operator/controllers/mcpserver_controller.go

Lines changed: 164 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
appsv1 "k8s.io/api/apps/v1"
1616
corev1 "k8s.io/api/core/v1"
17+
rbacv1 "k8s.io/api/rbac/v1"
1718
"k8s.io/apimachinery/pkg/api/errors"
1819
"k8s.io/apimachinery/pkg/api/resource"
1920
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -35,35 +36,44 @@ type MCPServerReconciler struct {
3536
Scheme *runtime.Scheme
3637
}
3738

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+
}
5768

58-
// Allow the operator to manage events
59-
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
69+
var ctxLogger = log.FromContext(context.Background())
6070

6171
// Reconcile is part of the main kubernetes reconciliation loop which aims to
6272
// move the current state of the cluster closer to the desired state.
6373
//
6474
//nolint:gocyclo
6575
func (r *MCPServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
66-
ctxLogger := log.FromContext(ctx)
76+
ctxLogger = log.FromContext(ctx)
6777

6878
// Fetch the MCPServer instance
6979
mcpServer := &mcpv1alpha1.MCPServer{}
@@ -115,6 +125,12 @@ func (r *MCPServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
115125
return ctrl.Result{}, err
116126
}
117127

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+
118134
// Check if the deployment already exists, if not create a new one
119135
deployment := &appsv1.Deployment{}
120136
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) (
219235
return ctrl.Result{}, nil
220236
}
221237

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+
222363
// deploymentForMCPServer returns a MCPServer Deployment object
223364
//
224365
//nolint:gocyclo
@@ -383,7 +524,7 @@ func (r *MCPServerReconciler) deploymentForMCPServer(m *mcpv1alpha1.MCPServer) *
383524
Labels: ls, // Keep original labels for pod template
384525
},
385526
Spec: corev1.PodSpec{
386-
ServiceAccountName: "toolhive",
527+
ServiceAccountName: fmt.Sprintf("%s-proxy-runner", m.Name),
387528
Containers: []corev1.Container{{
388529
Image: getToolhiveRunnerImage(),
389530
Name: "toolhive",

0 commit comments

Comments
 (0)