Skip to content

Commit d2c3613

Browse files
committed
add option to skip Virtual MCP CRDs and controllers installation
1 parent 8cfe520 commit d2c3613

39 files changed

+378
-33
lines changed

.github/workflows/operator-ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ jobs:
9595
- name: Check for changes
9696
id: git-check
9797
run: |
98-
git diff --exit-code deploy/charts/operator-crds/crds || echo "crd-changes=true" >> $GITHUB_OUTPUT
98+
git diff --exit-code deploy/charts/operator-crds/crd-files || echo "crd-changes=true" >> $GITHUB_OUTPUT
9999
git diff --exit-code deploy/charts/operator/templates || echo "operator-changes=true" >> $GITHUB_OUTPUT
100100
101101
- name: Fail if CRDs are not up to date

.github/workflows/test-helm-charts.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,13 @@ jobs:
3434
- name: Create KIND Cluster
3535
uses: helm/kind-action@92086f6be054225fa813e0a4b13787fc9088faab # v1.13.0
3636

37+
- name: Cleanup stale cluster-scoped RBAC before CT install
38+
run: |
39+
echo "Deleting stale cluster-scoped RBAC to avoid Helm ownership conflicts..."
40+
kubectl delete clusterrole toolhive-operator-manager-role --ignore-not-found=true
41+
kubectl delete clusterrole toolhive-registry-api-role --ignore-not-found=true
42+
kubectl delete clusterrolebinding toolhive-operator-manager-rolebinding --ignore-not-found=true
43+
kubectl delete clusterrolebinding toolhive-registry-api-rolebinding --ignore-not-found=true
44+
3745
- name: Run chart-testing (install)
3846
run: ct install --config ct-install.yaml

cmd/thv-operator/Taskfile.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ tasks:
184184
platforms: [windows]
185185
ignore_error: true # Windows has no mkdir -p, so just ignore error if it exists
186186
- go install sigs.k8s.io/controller-tools/cmd/[email protected]
187-
- $(go env GOPATH)/bin/controller-gen crd webhook paths="./cmd/thv-operator/..." output:crd:artifacts:config=deploy/charts/operator-crds/crds
187+
- $(go env GOPATH)/bin/controller-gen crd webhook paths="./cmd/thv-operator/..." output:crd:artifacts:config=deploy/charts/operator-crds/crd-files
188188
- $(go env GOPATH)/bin/controller-gen rbac:roleName=toolhive-operator-manager-role paths="./cmd/thv-operator/..." output:rbac:artifacts:config=deploy/charts/operator/templates/clusterrole
189189

190190
operator-test:

cmd/thv-operator/main.go

Lines changed: 118 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ import (
77
"flag"
88
"fmt"
99
"os"
10+
"strconv"
1011
"strings"
1112

13+
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
14+
// to ensure that exec-entrypoint and run can make use of them.
1215
"k8s.io/apimachinery/pkg/runtime"
1316
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
1417
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
15-
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
16-
// to ensure that exec-entrypoint and run can make use of them.
1718
_ "k8s.io/client-go/plugin/pkg/client/auth"
1819
ctrl "sigs.k8s.io/controller-runtime"
1920
"sigs.k8s.io/controller-runtime/pkg/cache"
@@ -36,6 +37,18 @@ var (
3637
setupLog = log.Log.WithName("setup")
3738
)
3839

40+
// Feature flags for controller groups
41+
const (
42+
featureServer = "ENABLE_SERVER"
43+
featureRegistry = "ENABLE_REGISTRY"
44+
featureVMCP = "ENABLE_VMCP"
45+
)
46+
47+
// controllerDependencies maps each controller group to its required dependencies
48+
var controllerDependencies = map[string][]string{
49+
featureVMCP: {featureServer}, // Virtual MCP requires server controllers
50+
}
51+
3952
func init() {
4053
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
4154
utilruntime.Must(mcpv1alpha1.AddToScheme(scheme))
@@ -111,6 +124,69 @@ func main() {
111124

112125
// setupControllersAndWebhooks sets up all controllers and webhooks with the manager
113126
func setupControllersAndWebhooks(mgr ctrl.Manager) error {
127+
// Check feature flags
128+
enableServer := isFeatureEnabled(featureServer, true)
129+
enableRegistry := isFeatureEnabled(featureRegistry, true)
130+
enableVMCP := isFeatureEnabled(featureVMCP, true)
131+
132+
// Track enabled features for dependency checking
133+
enabledFeatures := map[string]bool{
134+
featureServer: enableServer,
135+
featureRegistry: enableRegistry,
136+
featureVMCP: enableVMCP,
137+
}
138+
139+
// Check dependencies and log warnings for missing dependencies
140+
for feature, deps := range controllerDependencies {
141+
if !enabledFeatures[feature] {
142+
continue // Skip if feature itself is disabled
143+
}
144+
for _, dep := range deps {
145+
if !enabledFeatures[dep] {
146+
setupLog.Info(
147+
fmt.Sprintf("%s requires %s to be enabled, skipping %s controllers", feature, dep, feature),
148+
"feature", feature,
149+
"required_dependency", dep,
150+
)
151+
enabledFeatures[feature] = false // Mark as effectively disabled
152+
break
153+
}
154+
}
155+
}
156+
157+
// Set up server-related controllers
158+
if enabledFeatures[featureServer] {
159+
if err := setupServerControllers(mgr, enableRegistry); err != nil {
160+
return err
161+
}
162+
} else {
163+
setupLog.Info("ENABLE_SERVER is disabled, skipping server-related controllers")
164+
}
165+
166+
// Set up registry controller
167+
if enabledFeatures[featureRegistry] {
168+
if err := setupRegistryController(mgr); err != nil {
169+
return err
170+
}
171+
} else {
172+
setupLog.Info("ENABLE_REGISTRY is disabled, skipping MCPRegistry controller")
173+
}
174+
175+
// Set up Virtual MCP controllers and webhooks
176+
if enabledFeatures[featureVMCP] {
177+
if err := setupAggregationControllers(mgr); err != nil {
178+
return err
179+
}
180+
} else {
181+
setupLog.Info("ENABLE_VMCP is disabled, skipping Virtual MCP controllers and webhooks")
182+
}
183+
184+
//+kubebuilder:scaffold:builder
185+
return nil
186+
}
187+
188+
// setupServerControllers sets up server-related controllers (MCPServer, MCPExternalAuthConfig, MCPRemoteProxy, ToolConfig)
189+
func setupServerControllers(mgr ctrl.Manager, enableRegistry bool) error {
114190
// Set up field indexing for MCPServer.Spec.GroupRef
115191
if err := mgr.GetFieldIndexer().IndexField(
116192
context.Background(),
@@ -127,37 +203,43 @@ func setupControllersAndWebhooks(mgr ctrl.Manager) error {
127203
return fmt.Errorf("unable to create field index for spec.groupRef: %w", err)
128204
}
129205

130-
// Create a shared platform detector for all controllers
131-
sharedPlatformDetector := ctrlutil.NewSharedPlatformDetector()
206+
// Set image validation mode based on whether registry is enabled
207+
// If ENABLE_REGISTRY is enabled, enforce registry-based image validation
208+
// Otherwise, allow all images
209+
imageValidation := validation.ImageValidationAlwaysAllow
210+
if enableRegistry {
211+
imageValidation = validation.ImageValidationRegistryEnforcing
212+
}
213+
214+
// Set up MCPServer controller
132215
rec := &controllers.MCPServerReconciler{
133216
Client: mgr.GetClient(),
134217
Scheme: mgr.GetScheme(),
135218
Recorder: mgr.GetEventRecorderFor("mcpserver-controller"),
136-
PlatformDetector: sharedPlatformDetector,
137-
ImageValidation: validation.ImageValidationAlwaysAllow,
219+
PlatformDetector: ctrlutil.NewSharedPlatformDetector(),
220+
ImageValidation: imageValidation,
138221
}
139-
140222
if err := rec.SetupWithManager(mgr); err != nil {
141223
return fmt.Errorf("unable to create controller MCPServer: %w", err)
142224
}
143225

144-
// Register MCPToolConfig controller
226+
// Set up MCPToolConfig controller
145227
if err := (&controllers.ToolConfigReconciler{
146228
Client: mgr.GetClient(),
147229
Scheme: mgr.GetScheme(),
148230
}).SetupWithManager(mgr); err != nil {
149231
return fmt.Errorf("unable to create controller MCPToolConfig: %w", err)
150232
}
151233

152-
// Register MCPExternalAuthConfig controller
234+
// Set up MCPExternalAuthConfig controller
153235
if err := (&controllers.MCPExternalAuthConfigReconciler{
154236
Client: mgr.GetClient(),
155237
Scheme: mgr.GetScheme(),
156238
}).SetupWithManager(mgr); err != nil {
157239
return fmt.Errorf("unable to create controller MCPExternalAuthConfig: %w", err)
158240
}
159241

160-
// Register MCPRemoteProxy controller
242+
// Set up MCPRemoteProxy controller
161243
if err := (&controllers.MCPRemoteProxyReconciler{
162244
Client: mgr.GetClient(),
163245
Scheme: mgr.GetScheme(),
@@ -166,13 +248,22 @@ func setupControllersAndWebhooks(mgr ctrl.Manager) error {
166248
return fmt.Errorf("unable to create controller MCPRemoteProxy: %w", err)
167249
}
168250

169-
// Only register MCPRegistry controller if feature flag is enabled
170-
rec.ImageValidation = validation.ImageValidationRegistryEnforcing
251+
return nil
252+
}
171253

254+
// setupRegistryController sets up the MCPRegistry controller
255+
func setupRegistryController(mgr ctrl.Manager) error {
172256
if err := (controllers.NewMCPRegistryReconciler(mgr.GetClient(), mgr.GetScheme())).SetupWithManager(mgr); err != nil {
173257
return fmt.Errorf("unable to create controller MCPRegistry: %w", err)
174258
}
259+
return nil
260+
}
175261

262+
// setupAggregationControllers sets up Virtual MCP-related controllers and webhooks
263+
// (MCPGroup, VirtualMCPServer, and their webhooks)
264+
// Note: This function assumes server controllers are enabled (enforced by dependency check)
265+
// The field index for MCPServer.Spec.GroupRef is created in setupServerControllers
266+
func setupAggregationControllers(mgr ctrl.Manager) error {
176267
// Set up MCPGroup controller
177268
if err := (&controllers.MCPGroupReconciler{
178269
Client: mgr.GetClient(),
@@ -199,11 +290,25 @@ func setupControllersAndWebhooks(mgr ctrl.Manager) error {
199290
if err := (&mcpv1alpha1.VirtualMCPCompositeToolDefinition{}).SetupWebhookWithManager(mgr); err != nil {
200291
return fmt.Errorf("unable to create webhook VirtualMCPCompositeToolDefinition: %w", err)
201292
}
202-
//+kubebuilder:scaffold:builder
203293

204294
return nil
205295
}
206296

297+
// isFeatureEnabled checks if a feature flag environment variable is enabled.
298+
// If the environment variable is not set, it returns the default value.
299+
// The environment variable is considered enabled if it's set to "true" (case-insensitive).
300+
func isFeatureEnabled(envVar string, defaultValue bool) bool {
301+
value, found := os.LookupEnv(envVar)
302+
if !found {
303+
return defaultValue
304+
}
305+
enabled, err := strconv.ParseBool(value)
306+
if err != nil {
307+
return defaultValue
308+
}
309+
return enabled
310+
}
311+
207312
// getDefaultNamespaces returns a map of namespaces to cache.Config for the operator to watch.
208313
// if WATCH_NAMESPACE is not set, returns nil which is defaulted to a cluster scope.
209314
func getDefaultNamespaces() map[string]cache.Config {

cmd/thv-operator/test-integration/mcp-external-auth/suite_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ var _ = BeforeSuite(func() {
5757

5858
By("bootstrapping test environment")
5959
testEnv = &envtest.Environment{
60-
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crds")},
60+
CRDDirectoryPaths: []string{
61+
filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crd-files"),
62+
},
6163
ErrorIfCRDPathMissing: true,
6264
}
6365

cmd/thv-operator/test-integration/mcp-group/suite_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ var _ = BeforeSuite(func() {
5858

5959
By("bootstrapping test environment")
6060
testEnv = &envtest.Environment{
61-
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crds")},
61+
CRDDirectoryPaths: []string{
62+
filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crd-files"),
63+
},
6264
ErrorIfCRDPathMissing: true,
6365
}
6466

cmd/thv-operator/test-integration/mcp-registry/suite_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ var _ = BeforeSuite(func() {
6868
testEnv = &envtest.Environment{
6969
UseExistingCluster: &useExistingCluster,
7070
CRDDirectoryPaths: []string{
71-
filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crds"),
71+
filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crd-files"),
7272
},
7373
ErrorIfCRDPathMissing: true,
7474
BinaryAssetsDirectory: kubebuilderAssets,

cmd/thv-operator/test-integration/mcp-server/suite_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ var _ = BeforeSuite(func() {
6161

6262
By("bootstrapping test environment")
6363
testEnv = &envtest.Environment{
64-
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crds")},
64+
CRDDirectoryPaths: []string{
65+
filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crd-files"),
66+
},
6567
ErrorIfCRDPathMissing: true,
6668
}
6769

cmd/thv-operator/test-integration/virtualmcp/suite_test.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ var _ = BeforeSuite(func() {
6161

6262
By("bootstrapping test environment")
6363
testEnv = &envtest.Environment{
64-
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crds")},
64+
CRDDirectoryPaths: []string{
65+
filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crd-files"),
66+
},
6567
ErrorIfCRDPathMissing: true,
6668
}
6769

@@ -125,6 +127,14 @@ var _ = BeforeSuite(func() {
125127
}).SetupWithManager(k8sManager)
126128
Expect(err).ToNot(HaveOccurred())
127129

130+
// Set up VirtualMCPServer webhook
131+
err = (&mcpv1alpha1.VirtualMCPServer{}).SetupWebhookWithManager(k8sManager)
132+
Expect(err).ToNot(HaveOccurred())
133+
134+
// Set up VirtualMCPCompositeToolDefinition webhook
135+
err = (&mcpv1alpha1.VirtualMCPCompositeToolDefinition{}).SetupWebhookWithManager(k8sManager)
136+
Expect(err).ToNot(HaveOccurred())
137+
128138
// Start the manager in a goroutine
129139
go func() {
130140
defer GinkgoRecover()

ct-install.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Configuration for chart-testing (ct) install command
22
# See: https://github.com/helm/chart-testing
33

4+
skip-clean-up: true
45
charts:
56
- deploy/charts/operator-crds
67
- deploy/charts/operator

0 commit comments

Comments
 (0)