Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions cmd/thv-operator/controllers/mcpregistry_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ type MCPRegistryReconciler struct {
}

// NewMCPRegistryReconciler creates a new MCPRegistryReconciler with required dependencies
func NewMCPRegistryReconciler(k8sClient client.Client, scheme *runtime.Scheme) *MCPRegistryReconciler {
registryAPIManager := registryapi.NewManager(k8sClient, scheme)
func NewMCPRegistryReconciler(k8sClient client.Client, scheme *runtime.Scheme, disableWorkloadRBAC bool) *MCPRegistryReconciler {
registryAPIManager := registryapi.NewManager(k8sClient, scheme, disableWorkloadRBAC)
return &MCPRegistryReconciler{
Client: k8sClient,
Scheme: scheme,
Expand All @@ -67,10 +67,6 @@ func NewMCPRegistryReconciler(k8sClient client.Client, scheme *runtime.Scheme) *
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete
//
// For creating registry-api RBAC resources
// +kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles;rolebindings,verbs=get;list;watch;create;update;patch;delete
//
// For granting registry-api permissions (operator must have these to grant them via Role)
// +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=mcpservers;mcpremoteproxies;virtualmcpservers,verbs=get;list;watch
// +kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=httproutes;gateways,verbs=get;list;watch
Expand Down
14 changes: 8 additions & 6 deletions cmd/thv-operator/controllers/mcpremoteproxy_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ import (
// MCPRemoteProxyReconciler reconciles a MCPRemoteProxy object
type MCPRemoteProxyReconciler struct {
client.Client
Scheme *runtime.Scheme
Recorder record.EventRecorder
PlatformDetector *ctrlutil.SharedPlatformDetector
Scheme *runtime.Scheme
Recorder record.EventRecorder
PlatformDetector *ctrlutil.SharedPlatformDetector
DisableWorkloadRBAC bool
}

// +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=mcpremoteproxies,verbs=get;list;watch;create;update;patch;delete
Expand All @@ -48,12 +49,9 @@ type MCPRemoteProxyReconciler struct {
// +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=mcpexternalauthconfigs,verbs=get;list;watch
// +kubebuilder:rbac:groups="",resources=configmaps,verbs=create;delete;get;list;patch;update;watch
// +kubebuilder:rbac:groups="",resources=services,verbs=create;delete;get;list;patch;update;watch
// +kubebuilder:rbac:groups="rbac.authorization.k8s.io",resources=roles,verbs=create;delete;get;list;patch;update;watch
// +kubebuilder:rbac:groups="rbac.authorization.k8s.io",resources=rolebindings,verbs=create;delete;get;list;patch;update;watch
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=create;delete;get;list;patch;update;watch
// +kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=create;delete;get;list;patch;update;watch

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
Expand Down Expand Up @@ -584,6 +582,10 @@ func (r *MCPRemoteProxyReconciler) validateGroupRef(ctx context.Context, proxy *
// Uses the RBAC client (pkg/kubernetes/rbac) which creates or updates RBAC resources
// automatically during operator upgrades.
func (r *MCPRemoteProxyReconciler) ensureRBACResources(ctx context.Context, proxy *mcpv1alpha1.MCPRemoteProxy) error {
if r.DisableWorkloadRBAC {
return nil
}

// If a service account is specified, we don't need to create one
if proxy.Spec.ServiceAccount != nil {
return nil
Expand Down
44 changes: 44 additions & 0 deletions cmd/thv-operator/controllers/mcpremoteproxy_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1099,6 +1099,50 @@ func TestMCPRemoteProxyEnsureRBACResources_CustomServiceAccount(t *testing.T) {
assert.Error(t, err, "RoleBinding should not be created when custom ServiceAccount is provided")
}

// TestEnsureRBACResources_DisableWorkloadRBAC_MCPRemoteProxy tests that RBAC creation is skipped
// when DisableWorkloadRBAC is true.
func TestEnsureRBACResources_DisableWorkloadRBAC_MCPRemoteProxy(t *testing.T) {
t.Parallel()

proxy := &mcpv1alpha1.MCPRemoteProxy{
ObjectMeta: metav1.ObjectMeta{
Name: "test-proxy-disable-rbac",
Namespace: "default",
},
Spec: mcpv1alpha1.MCPRemoteProxySpec{
RemoteURL: "https://mcp.example.com",
Port: 8080,
},
}

testScheme := createRunConfigTestScheme()
_ = rbacv1.AddToScheme(testScheme)

fakeClient := fake.NewClientBuilder().
WithScheme(testScheme).
WithRuntimeObjects(proxy).
Build()

reconciler := &MCPRemoteProxyReconciler{
Client: fakeClient,
Scheme: testScheme,
DisableWorkloadRBAC: true,
}

err := reconciler.ensureRBACResources(t.Context(), proxy)
require.NoError(t, err)

// Verify NO RBAC resources were created
generatedSAName := proxyRunnerServiceAccountNameForRemoteProxy(proxy.Name)

sa := &corev1.ServiceAccount{}
err = fakeClient.Get(t.Context(), types.NamespacedName{
Name: generatedSAName,
Namespace: proxy.Namespace,
}, sa)
assert.Error(t, err, "ServiceAccount should not be created when workload RBAC is disabled")
}

// TestUpdateMCPRemoteProxyStatus tests status update logic
func TestUpdateMCPRemoteProxyStatus(t *testing.T) {
t.Parallel()
Expand Down
17 changes: 10 additions & 7 deletions cmd/thv-operator/controllers/mcpserver_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,11 @@ import (
// MCPServerReconciler reconciles a MCPServer object
type MCPServerReconciler struct {
client.Client
Scheme *runtime.Scheme
Recorder record.EventRecorder
PlatformDetector *ctrlutil.SharedPlatformDetector
ImageValidation validation.ImageValidation
Scheme *runtime.Scheme
Recorder record.EventRecorder
PlatformDetector *ctrlutil.SharedPlatformDetector
ImageValidation validation.ImageValidation
DisableWorkloadRBAC bool
}

// defaultRBACRules are the default RBAC rules that the
Expand Down Expand Up @@ -141,13 +142,10 @@ func (r *MCPServerReconciler) detectPlatform(ctx context.Context) (kubernetes.Pl
// +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=mcptoolconfigs,verbs=get;list;watch
// +kubebuilder:rbac:groups="",resources=configmaps,verbs=create;delete;get;list;patch;update;watch
// +kubebuilder:rbac:groups="",resources=services,verbs=create;delete;get;list;patch;update;watch
// +kubebuilder:rbac:groups="rbac.authorization.k8s.io",resources=roles,verbs=create;delete;get;list;patch;update;watch
// +kubebuilder:rbac:groups="rbac.authorization.k8s.io",resources=rolebindings,verbs=create;delete;get;list;patch;update;watch
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
// +kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=create;delete;get;list;patch;update;watch
// +kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=create;delete;get;list;patch;update;watch
// +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups="",resources=pods/attach,verbs=create;get
// +kubebuilder:rbac:groups="",resources=pods/log,verbs=get
Expand Down Expand Up @@ -884,7 +882,12 @@ func (r *MCPServerReconciler) handleToolConfig(ctx context.Context, m *mcpv1alph

return nil
}

func (r *MCPServerReconciler) ensureRBACResources(ctx context.Context, mcpServer *mcpv1alpha1.MCPServer) error {
if r.DisableWorkloadRBAC {
return nil
}

rbacClient := rbac.NewClient(r.Client, r.Scheme)
proxyRunnerNameForRBAC := ctrlutil.ProxyRunnerServiceAccountName(mcpServer.Name)

Expand Down
17 changes: 17 additions & 0 deletions cmd/thv-operator/controllers/mcpserver_rbac_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,23 @@ func TestEnsureRBACResources_ImagePullSecrets(t *testing.T) {
assert.Equal(t, expectedSecrets, mcpServerSA.ImagePullSecrets)
}

func TestEnsureRBACResources_DisableWorkloadRBAC(t *testing.T) {
t.Parallel()
tc := setupTest("test-server-disable-rbac", "default")
tc.reconciler.DisableWorkloadRBAC = true

err := tc.ensureRBACResources()
require.NoError(t, err)

// Verify NO RBAC resources were created
sa := &corev1.ServiceAccount{}
err = tc.client.Get(t.Context(), types.NamespacedName{
Name: tc.proxyRunnerNameForRBAC,
Namespace: tc.mcpServer.Namespace,
}, sa)
assert.Error(t, err, "ServiceAccount should not be created when workload RBAC is disabled")
}

func createTestMCPServer(name, namespace string) *mcpv1alpha1.MCPServer {
return &mcpv1alpha1.MCPServer{
ObjectMeta: metav1.ObjectMeta{
Expand Down
14 changes: 8 additions & 6 deletions cmd/thv-operator/controllers/virtualmcpserver_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,10 @@ type AuthConfigError struct {
// may not have owner references (StatefulSet, headless Service, RunConfig ConfigMap).
type VirtualMCPServerReconciler struct {
client.Client
Scheme *runtime.Scheme
Recorder record.EventRecorder
PlatformDetector *ctrlutil.SharedPlatformDetector
Scheme *runtime.Scheme
Recorder record.EventRecorder
PlatformDetector *ctrlutil.SharedPlatformDetector
DisableWorkloadRBAC bool
}

// +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=virtualmcpservers,verbs=get;list;watch;create;update;patch;delete
Expand All @@ -97,12 +98,9 @@ type VirtualMCPServerReconciler struct {
// +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=virtualmcpcompositetooldefinitions,verbs=get;list;watch
// +kubebuilder:rbac:groups="",resources=configmaps,verbs=create;delete;get;list;patch;update;watch
// +kubebuilder:rbac:groups="",resources=services,verbs=create;delete;get;list;patch;update;watch
// +kubebuilder:rbac:groups="rbac.authorization.k8s.io",resources=roles,verbs=create;delete;get;list;patch;update;watch
// +kubebuilder:rbac:groups="rbac.authorization.k8s.io",resources=rolebindings,verbs=create;delete;get;list;patch;update;watch
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=create;delete;get;list;patch;update;watch
// +kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=create;delete;get;list;patch;update;watch
// +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=embeddingservers,verbs=get;list;watch
// +kubebuilder:rbac:groups=toolhive.stacklok.dev,resources=embeddingservers/status,verbs=get

Expand Down Expand Up @@ -630,6 +628,10 @@ func (r *VirtualMCPServerReconciler) ensureRBACResources(
ctx context.Context,
vmcp *mcpv1alpha1.VirtualMCPServer,
) error {
if r.DisableWorkloadRBAC {
return nil
}

// If a service account is specified, we don't need to create one
if vmcp.Spec.ServiceAccount != nil {
return nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,52 @@ func TestVirtualMCPServerEnsureRBACResources_CustomServiceAccount(t *testing.T)
assert.Error(t, err, "RoleBinding should not be created when custom ServiceAccount is provided")
}

// TestVirtualMCPServerEnsureRBACResources_DisableWorkloadRBAC tests that RBAC creation is skipped
// when DisableWorkloadRBAC is true.
func TestVirtualMCPServerEnsureRBACResources_DisableWorkloadRBAC(t *testing.T) {
t.Parallel()

vmcp := &mcpv1alpha1.VirtualMCPServer{
ObjectMeta: metav1.ObjectMeta{
Name: "test-vmcp-disable-rbac",
Namespace: "default",
UID: "test-uid",
},
Spec: mcpv1alpha1.VirtualMCPServerSpec{
Config: vmcpconfig.Config{Group: testGroupName},
},
}

scheme := runtime.NewScheme()
_ = mcpv1alpha1.AddToScheme(scheme)
_ = corev1.AddToScheme(scheme)
_ = rbacv1.AddToScheme(scheme)

fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(vmcp).
Build()

r := &VirtualMCPServerReconciler{
Client: fakeClient,
Scheme: scheme,
DisableWorkloadRBAC: true,
}

err := r.ensureRBACResources(t.Context(), vmcp)
require.NoError(t, err)

// Verify NO RBAC resources were created
generatedSAName := vmcpServiceAccountName(vmcp.Name)

sa := &corev1.ServiceAccount{}
err = fakeClient.Get(t.Context(), types.NamespacedName{
Name: generatedSAName,
Namespace: vmcp.Namespace,
}, sa)
assert.Error(t, err, "ServiceAccount should not be created when workload RBAC is disabled")
}

// TestVirtualMCPServerEnsureDeployment tests Deployment creation
func TestVirtualMCPServerEnsureDeployment(t *testing.T) {
t.Parallel()
Expand Down
Loading
Loading