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
2 changes: 1 addition & 1 deletion .github/workflows/operator-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ jobs:
- name: Check for changes
id: git-check
run: |
git diff --exit-code deploy/charts/operator-crds/crds || echo "crd-changes=true" >> $GITHUB_OUTPUT
git diff --exit-code deploy/charts/operator-crds/crds deploy/charts/operator-crds/crds-server deploy/charts/operator-crds/crds-registry deploy/charts/operator-crds/crds-virtualmcp || echo "crd-changes=true" >> $GITHUB_OUTPUT
git diff --exit-code deploy/charts/operator/templates || echo "operator-changes=true" >> $GITHUB_OUTPUT
- name: Fail if CRDs are not up to date
Expand Down
37 changes: 37 additions & 0 deletions cmd/thv-operator/Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,43 @@ tasks:
- go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.17.3
- $(go env GOPATH)/bin/controller-gen crd webhook paths="./cmd/thv-operator/..." output:crd:artifacts:config=deploy/charts/operator-crds/crds
- $(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
# Organize CRDs into feature-specific directories
- cmd: mkdir -p deploy/charts/operator-crds/crds-server deploy/charts/operator-crds/crds-registry deploy/charts/operator-crds/crds-virtualmcp
platforms: [linux, darwin]
- cmd: cmd.exe /c mkdir deploy\charts\operator-crds\crds-server deploy\charts\operator-crds\crds-registry deploy\charts\operator-crds\crds-virtualmcp
platforms: [windows]
ignore_error: true
# Move server CRDs
- cmd: |
mv deploy/charts/operator-crds/crds/toolhive.stacklok.dev_mcpservers.yaml deploy/charts/operator-crds/crds-server/ 2>/dev/null || true
mv deploy/charts/operator-crds/crds/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml deploy/charts/operator-crds/crds-server/ 2>/dev/null || true
mv deploy/charts/operator-crds/crds/toolhive.stacklok.dev_mcpremoteproxies.yaml deploy/charts/operator-crds/crds-server/ 2>/dev/null || true
mv deploy/charts/operator-crds/crds/toolhive.stacklok.dev_mcptoolconfigs.yaml deploy/charts/operator-crds/crds-server/ 2>/dev/null || true
platforms: [linux, darwin]
- cmd: |
if exist deploy\charts\operator-crds\crds\toolhive.stacklok.dev_mcpservers.yaml move deploy\charts\operator-crds\crds\toolhive.stacklok.dev_mcpservers.yaml deploy\charts\operator-crds\crds-server\
if exist deploy\charts\operator-crds\crds\toolhive.stacklok.dev_mcpexternalauthconfigs.yaml move deploy\charts\operator-crds\crds\toolhive.stacklok.dev_mcpexternalauthconfigs.yaml deploy\charts\operator-crds\crds-server\
if exist deploy\charts\operator-crds\crds\toolhive.stacklok.dev_mcpremoteproxies.yaml move deploy\charts\operator-crds\crds\toolhive.stacklok.dev_mcpremoteproxies.yaml deploy\charts\operator-crds\crds-server\
if exist deploy\charts\operator-crds\crds\toolhive.stacklok.dev_mcptoolconfigs.yaml move deploy\charts\operator-crds\crds\toolhive.stacklok.dev_mcptoolconfigs.yaml deploy\charts\operator-crds\crds-server\
platforms: [windows]
# Move registry CRD
- cmd: |
mv deploy/charts/operator-crds/crds/toolhive.stacklok.dev_mcpregistries.yaml deploy/charts/operator-crds/crds-registry/ 2>/dev/null || true
platforms: [linux, darwin]
- cmd: |
if exist deploy\charts\operator-crds\crds\toolhive.stacklok.dev_mcpregistries.yaml move deploy\charts\operator-crds\crds\toolhive.stacklok.dev_mcpregistries.yaml deploy\charts\operator-crds\crds-registry\
platforms: [windows]
# Move Virtual MCP CRDs
- cmd: |
mv deploy/charts/operator-crds/crds/toolhive.stacklok.dev_virtualmcpservers.yaml deploy/charts/operator-crds/crds-virtualmcp/ 2>/dev/null || true
mv deploy/charts/operator-crds/crds/toolhive.stacklok.dev_virtualmcpcompositetooldefinitions.yaml deploy/charts/operator-crds/crds-virtualmcp/ 2>/dev/null || true
mv deploy/charts/operator-crds/crds/toolhive.stacklok.dev_mcpgroups.yaml deploy/charts/operator-crds/crds-virtualmcp/ 2>/dev/null || true
platforms: [linux, darwin]
- cmd: |
if exist deploy\charts\operator-crds\crds\toolhive.stacklok.dev_virtualmcpservers.yaml move deploy\charts\operator-crds\crds\toolhive.stacklok.dev_virtualmcpservers.yaml deploy\charts\operator-crds\crds-virtualmcp\
if exist deploy\charts\operator-crds\crds\toolhive.stacklok.dev_virtualmcpcompositetooldefinitions.yaml move deploy\charts\operator-crds\crds\toolhive.stacklok.dev_virtualmcpcompositetooldefinitions.yaml deploy\charts\operator-crds\crds-virtualmcp\
if exist deploy\charts\operator-crds\crds\toolhive.stacklok.dev_mcpgroups.yaml move deploy\charts\operator-crds\crds\toolhive.stacklok.dev_mcpgroups.yaml deploy\charts\operator-crds\crds-virtualmcp\
platforms: [windows]

operator-test:
desc: Run tests for the operator
Expand Down
115 changes: 92 additions & 23 deletions cmd/thv-operator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import (
"os"
"strings"

// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
// to ensure that exec-entrypoint and run can make use of them.
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
// to ensure that exec-entrypoint and run can make use of them.
_ "k8s.io/client-go/plugin/pkg/client/auth"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/cache"
Expand Down Expand Up @@ -111,53 +111,88 @@ func main() {

// setupControllersAndWebhooks sets up all controllers and webhooks with the manager
func setupControllersAndWebhooks(mgr ctrl.Manager) error {
// Set up field indexing for MCPServer.Spec.GroupRef
if err := mgr.GetFieldIndexer().IndexField(
context.Background(),
&mcpv1alpha1.MCPServer{},
"spec.groupRef",
func(obj client.Object) []string {
mcpServer := obj.(*mcpv1alpha1.MCPServer)
if mcpServer.Spec.GroupRef == "" {
return nil
// Check feature flags
enableServer := isFeatureEnabled("ENABLE_SERVER", true)
enableRegistry := isFeatureEnabled("ENABLE_REGISTRY", true)
enableAggregation := isFeatureEnabled("ENABLE_AGGREGATION", true)

// Set up server-related controllers
if enableServer {
if err := setupServerControllers(mgr, enableRegistry); err != nil {
return err
}
} else {
setupLog.Info("ENABLE_SERVER is disabled, skipping server-related controllers")
}

// Set up registry controller
if enableRegistry {
if err := setupRegistryController(mgr); err != nil {
return err
}
} else {
setupLog.Info("ENABLE_REGISTRY is disabled, skipping MCPRegistry controller")
}

// Set up aggregation controllers and webhooks
if enableAggregation {
if !enableServer {
setupLog.Info("ENABLE_AGGREGATION requires ENABLE_SERVER to be enabled, skipping aggregation controllers")
} else {
if err := setupAggregationControllers(mgr); err != nil {
return err
}
return []string{mcpServer.Spec.GroupRef}
},
); err != nil {
return fmt.Errorf("unable to create field index for spec.groupRef: %w", err)
}
} else {
setupLog.Info("ENABLE_AGGREGATION is disabled, skipping Virtual MCP controllers and webhooks")
}

//+kubebuilder:scaffold:builder
return nil
}

// setupServerControllers sets up server-related controllers (MCPServer, MCPExternalAuthConfig, MCPRemoteProxy, ToolConfig)
func setupServerControllers(mgr ctrl.Manager, enableRegistry bool) error {
// Create a shared platform detector for all controllers
sharedPlatformDetector := ctrlutil.NewSharedPlatformDetector()

// Set image validation mode based on whether registry is enabled
// If ENABLE_REGISTRY is enabled, enforce registry-based image validation
// Otherwise, allow all images
imageValidation := validation.ImageValidationAlwaysAllow
if enableRegistry {
imageValidation = validation.ImageValidationRegistryEnforcing
}

// Set up MCPServer controller
rec := &controllers.MCPServerReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("mcpserver-controller"),
PlatformDetector: sharedPlatformDetector,
ImageValidation: validation.ImageValidationAlwaysAllow,
ImageValidation: imageValidation,
}

if err := rec.SetupWithManager(mgr); err != nil {
return fmt.Errorf("unable to create controller MCPServer: %w", err)
}

// Register MCPToolConfig controller
// Set up MCPToolConfig controller
if err := (&controllers.ToolConfigReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
return fmt.Errorf("unable to create controller MCPToolConfig: %w", err)
}

// Register MCPExternalAuthConfig controller
// Set up MCPExternalAuthConfig controller
if err := (&controllers.MCPExternalAuthConfigReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
return fmt.Errorf("unable to create controller MCPExternalAuthConfig: %w", err)
}

// Register MCPRemoteProxy controller
// Set up MCPRemoteProxy controller
if err := (&controllers.MCPRemoteProxyReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Expand All @@ -166,12 +201,36 @@ func setupControllersAndWebhooks(mgr ctrl.Manager) error {
return fmt.Errorf("unable to create controller MCPRemoteProxy: %w", err)
}

// Only register MCPRegistry controller if feature flag is enabled
rec.ImageValidation = validation.ImageValidationRegistryEnforcing
return nil
}

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

// setupAggregationControllers sets up aggregation-related controllers and webhooks
// (MCPGroup, VirtualMCPServer, and their webhooks)
func setupAggregationControllers(mgr ctrl.Manager) error {
// Set up field indexing for MCPServer.Spec.GroupRef
// This is needed for MCPGroup controller to find MCPServers that belong to a group
if err := mgr.GetFieldIndexer().IndexField(
context.Background(),
&mcpv1alpha1.MCPServer{},
"spec.groupRef",
func(obj client.Object) []string {
mcpServer := obj.(*mcpv1alpha1.MCPServer)
if mcpServer.Spec.GroupRef == "" {
return nil
}
return []string{mcpServer.Spec.GroupRef}
},
); err != nil {
return fmt.Errorf("unable to create field index for spec.groupRef: %w", err)
}

// Set up MCPGroup controller
if err := (&controllers.MCPGroupReconciler{
Expand Down Expand Up @@ -199,11 +258,21 @@ func setupControllersAndWebhooks(mgr ctrl.Manager) error {
if err := (&mcpv1alpha1.VirtualMCPCompositeToolDefinition{}).SetupWebhookWithManager(mgr); err != nil {
return fmt.Errorf("unable to create webhook VirtualMCPCompositeToolDefinition: %w", err)
}
//+kubebuilder:scaffold:builder

return nil
}

// isFeatureEnabled checks if a feature flag environment variable is enabled.
// If the environment variable is not set, it returns the default value.
// The environment variable is considered enabled if it's set to "true" (case-insensitive).
func isFeatureEnabled(envVar string, defaultValue bool) bool {
value, found := os.LookupEnv(envVar)
if !found {
return defaultValue
}
return strings.EqualFold(value, "true")
}

// getDefaultNamespaces returns a map of namespaces to cache.Config for the operator to watch.
// if WATCH_NAMESPACE is not set, returns nil which is defaulted to a cluster scope.
func getDefaultNamespaces() map[string]cache.Config {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ var _ = BeforeSuite(func() {

By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crds")},
CRDDirectoryPaths: []string{
filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crds-server"),
},
ErrorIfCRDPathMissing: true,
}

Expand Down
5 changes: 4 additions & 1 deletion cmd/thv-operator/test-integration/mcp-group/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ var _ = BeforeSuite(func() {

By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crds")},
CRDDirectoryPaths: []string{
filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crds-server"),
filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crds-virtualmcp"),
},
ErrorIfCRDPathMissing: true,
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ var _ = BeforeSuite(func() {
testEnv = &envtest.Environment{
UseExistingCluster: &useExistingCluster,
CRDDirectoryPaths: []string{
filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crds"),
filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crds-registry"),
},
ErrorIfCRDPathMissing: true,
BinaryAssetsDirectory: kubebuilderAssets,
Expand Down
5 changes: 4 additions & 1 deletion cmd/thv-operator/test-integration/mcp-server/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ var _ = BeforeSuite(func() {

By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crds")},
CRDDirectoryPaths: []string{
filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crds-server"),
filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crds-virtualmcp"),
},
ErrorIfCRDPathMissing: true,
}

Expand Down
13 changes: 12 additions & 1 deletion cmd/thv-operator/test-integration/virtualmcp/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ var _ = BeforeSuite(func() {

By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crds")},
CRDDirectoryPaths: []string{
filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crds-server"),
filepath.Join("..", "..", "..", "..", "deploy", "charts", "operator-crds", "crds-virtualmcp"),
},
ErrorIfCRDPathMissing: true,
}

Expand Down Expand Up @@ -125,6 +128,14 @@ var _ = BeforeSuite(func() {
}).SetupWithManager(k8sManager)
Expect(err).ToNot(HaveOccurred())

// Set up VirtualMCPServer webhook
err = (&mcpv1alpha1.VirtualMCPServer{}).SetupWebhookWithManager(k8sManager)
Expect(err).ToNot(HaveOccurred())

// Set up VirtualMCPCompositeToolDefinition webhook
err = (&mcpv1alpha1.VirtualMCPCompositeToolDefinition{}).SetupWebhookWithManager(k8sManager)
Expect(err).ToNot(HaveOccurred())

// Start the manager in a goroutine
go func() {
defer GinkgoRecover()
Expand Down
2 changes: 1 addition & 1 deletion deploy/charts/operator-crds/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ apiVersion: v2
name: toolhive-operator-crds
description: A Helm chart for installing the ToolHive Operator CRDs into Kubernetes.
type: application
version: 0.0.64
version: 0.0.65
appVersion: "0.0.1"
70 changes: 69 additions & 1 deletion deploy/charts/operator-crds/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ToolHive Operator CRDs Helm Chart

![Version: 0.0.64](https://img.shields.io/badge/Version-0.0.64-informational?style=flat-square)
![Version: 0.0.65](https://img.shields.io/badge/Version-0.0.65-informational?style=flat-square)
![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square)

A Helm chart for installing the ToolHive Operator CRDs into Kubernetes.
Expand Down Expand Up @@ -40,3 +40,71 @@ To uninstall/delete the `toolhive-operator-crds` deployment:
helm uninstall <release_name>
```

### Skipping CRDs

By default, all CRDs are installed. You can selectively disable CRD groups based on your needs:

#### Skipping Server CRDs

To skip server-related CRDs (MCPServer, MCPExternalAuthConfig, MCPRemoteProxy, and ToolConfig):

```shell
helm upgrade -i toolhive-operator-crds oci://ghcr.io/stacklok/toolhive/toolhive-operator-crds \
--set crds.install.server=false
```

**Important:** When server CRDs are not installed, you should also disable the server controllers in the operator:

```shell
helm upgrade -i toolhive-operator oci://ghcr.io/stacklok/toolhive/toolhive-operator \
-n toolhive-system --create-namespace \
--set operator.features.server=false
```

#### Skipping Registry CRD

To skip the registry CRD (MCPRegistry):

```shell
helm upgrade -i toolhive-operator-crds oci://ghcr.io/stacklok/toolhive/toolhive-operator-crds \
--set crds.install.registry=false
```

**Important:** When registry CRD is not installed, you should also disable the registry controller in the operator:

```shell
helm upgrade -i toolhive-operator oci://ghcr.io/stacklok/toolhive/toolhive-operator \
-n toolhive-system --create-namespace \
--set operator.features.registry=false
```

#### Skipping Virtual MCP CRDs

To skip Virtual MCP CRDs (VirtualMCPServer, VirtualMCPCompositeToolDefinition, and MCPGroup):

```shell
helm upgrade -i toolhive-operator-crds oci://ghcr.io/stacklok/toolhive/toolhive-operator-crds \
--set crds.install.virtualMCP=false
```

You can also combine this with disabling the registry CRD (see [Skipping Registry CRD](#skipping-registry-crd) section above) if you don't need registry features.

**Important:** When Virtual MCP CRDs are not installed, you should also disable the Virtual MCP controllers in the operator:

```shell
helm upgrade -i toolhive-operator oci://ghcr.io/stacklok/toolhive/toolhive-operator \
-n toolhive-system --create-namespace \
--set operator.features.virtualMCP=false
```

If you also disabled the registry CRD, disable the registry controller as well (see the [Skipping Registry CRD](#skipping-registry-crd) section above).

This saves approximately 54KB of CRDs and is useful for deployments that don't require Virtual MCP aggregation features. When `operator.features.virtualMCP=false`, the operator will skip setting up the VirtualMCPServer controller, MCPGroup controller, and their associated webhooks.

## Values

| Key | Type | Default | Description |
|-----|-------------|------|---------|
| crds.install.registry | bool | `true` | Install registry CRD (MCPRegistry). Users who only need server management without registry features can set this to false to skip installing the registry CRD. |
| crds.install.server | bool | `true` | Install server-related CRDs (MCPServer, MCPExternalAuthConfig, MCPRemoteProxy, and ToolConfig). Users who only need registry or aggregation features can set this to false to skip installing server management CRDs. |
| crds.install.virtualMCP | bool | `true` | Install Virtual MCP CRDs (VirtualMCPServer and VirtualMCPCompositeToolDefinition). Users who only need core MCP server management can set this to false to skip installing Virtual MCP aggregation features (saves ~54KB of CRDs). |
Loading
Loading