Skip to content
Merged
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 cmd/thv-operator/controllers/mcpserver_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -792,7 +792,7 @@ func (r *MCPServerReconciler) handleToolConfig(ctx context.Context, m *mcpv1alph
}

// Get the referenced MCPToolConfig
toolConfig, err := GetToolConfigForMCPServer(ctx, r.Client, m)
toolConfig, err := ctrlutil.GetToolConfigForMCPServer(ctx, r.Client, m)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/thv-operator/controllers/mcpserver_runconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func (r *MCPServerReconciler) createRunConfigFromMCPServer(m *mcpv1alpha1.MCPSer

if m.Spec.ToolConfigRef != nil {
// ToolConfigRef takes precedence over inline ToolsFilter
toolConfig, err := GetToolConfigForMCPServer(context.Background(), r.Client, m)
toolConfig, err := ctrlutil.GetToolConfigForMCPServer(context.Background(), r.Client, m)
if err != nil {
return nil, fmt.Errorf("failed to get MCPToolConfig: %w", err)
}
Expand Down
29 changes: 0 additions & 29 deletions cmd/thv-operator/controllers/toolconfig_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,32 +227,3 @@ func (r *ToolConfigReconciler) SetupWithManager(mgr ctrl.Manager) error {
Watches(&mcpv1alpha1.MCPServer{}, toolConfigHandler).
Complete(r)
}

// GetToolConfigForMCPServer retrieves the MCPToolConfig referenced by an MCPServer
func GetToolConfigForMCPServer(
ctx context.Context,
c client.Client,
mcpServer *mcpv1alpha1.MCPServer,
) (*mcpv1alpha1.MCPToolConfig, error) {
if mcpServer.Spec.ToolConfigRef == nil {
// We throw an error because in this case you assume there is a ToolConfig
// but there isn't one referenced.
return nil, fmt.Errorf("MCPServer %s does not reference a MCPToolConfig", mcpServer.Name)
}

toolConfig := &mcpv1alpha1.MCPToolConfig{}
err := c.Get(ctx, types.NamespacedName{
Name: mcpServer.Spec.ToolConfigRef.Name,
Namespace: mcpServer.Namespace, // Same namespace as MCPServer
}, toolConfig)

if err != nil {
if errors.IsNotFound(err) {
return nil, fmt.Errorf("MCPToolConfig %s not found in namespace %s",
mcpServer.Spec.ToolConfigRef.Name, mcpServer.Namespace)
}
return nil, fmt.Errorf("failed to get MCPToolConfig: %w", err)
}

return toolConfig, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@ import (

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
Expand Down Expand Up @@ -246,94 +244,6 @@ func (c *errorClient) List(ctx context.Context, list client.ObjectList, opts ...
return c.Client.List(ctx, list, opts...)
}

func TestGetToolConfigForMCPServer_ErrorScenarios(t *testing.T) {
t.Parallel()

t.Run("toolconfig not found returns formatted error", func(t *testing.T) {
t.Parallel()
ctx := t.Context()

scheme := runtime.NewScheme()
require.NoError(t, mcpv1alpha1.AddToScheme(scheme))

mcpServer := &mcpv1alpha1.MCPServer{
ObjectMeta: metav1.ObjectMeta{
Name: "test-server",
Namespace: "default",
},
Spec: mcpv1alpha1.MCPServerSpec{
Image: "test-image",
ToolConfigRef: &mcpv1alpha1.ToolConfigRef{
Name: "missing-config",
},
},
}

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

config, err := GetToolConfigForMCPServer(ctx, fakeClient, mcpServer)
assert.Error(t, err)
assert.Nil(t, config)
assert.Contains(t, err.Error(), "MCPToolConfig missing-config not found")
assert.Contains(t, err.Error(), "namespace default")
})

t.Run("generic error is wrapped", func(t *testing.T) {
t.Parallel()

ctx := t.Context()

scheme := runtime.NewScheme()
require.NoError(t, mcpv1alpha1.AddToScheme(scheme))

mcpServer := &mcpv1alpha1.MCPServer{
ObjectMeta: metav1.ObjectMeta{
Name: "test-server",
Namespace: "default",
},
Spec: mcpv1alpha1.MCPServerSpec{
Image: "test-image",
ToolConfigRef: &mcpv1alpha1.ToolConfigRef{
Name: "test-config",
},
},
}

// Create a client that returns a generic error
fakeClient := &errorGetClient{
Client: fake.NewClientBuilder().
WithScheme(scheme).
Build(),
getError: errors.New("network error"),
}

config, err := GetToolConfigForMCPServer(ctx, fakeClient, mcpServer)
assert.Error(t, err)
assert.Nil(t, config)
assert.Contains(t, err.Error(), "failed to get MCPToolConfig")
assert.Contains(t, err.Error(), "network error")
})
}

// errorGetClient is a fake client that simulates Get errors
type errorGetClient struct {
client.Client
getError error
}

func (c *errorGetClient) Get(_ context.Context, key client.ObjectKey, _ client.Object, _ ...client.GetOption) error {
if c.getError != nil {
return c.getError
}
// Return not found error
return apierrors.NewNotFound(schema.GroupResource{
Group: "toolhive.stacklok.dev",
Resource: "toolconfigs",
}, key.Name)
}

func TestToolConfigReconciler_ComplexScenarios(t *testing.T) {
t.Parallel()

Expand Down
106 changes: 0 additions & 106 deletions cmd/thv-operator/controllers/toolconfig_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,109 +304,3 @@ func TestToolConfigReconciler_findReferencingMCPServers(t *testing.T) {
assert.Contains(t, serverNames, "server2")
assert.NotContains(t, serverNames, "server3")
}

func TestGetToolConfigForMCPServer(t *testing.T) {
t.Parallel()

tests := []struct {
name string
mcpServer *mcpv1alpha1.MCPServer
existingConfig *mcpv1alpha1.MCPToolConfig
expectConfig bool
expectError bool
}{
{
name: "mcpserver without toolconfig ref",
mcpServer: &mcpv1alpha1.MCPServer{
ObjectMeta: metav1.ObjectMeta{
Name: "test-server",
Namespace: "default",
},
Spec: mcpv1alpha1.MCPServerSpec{
Image: "test-image",
},
},
expectConfig: false,
expectError: true, // Changed to expect an error when no ToolConfigRef is present
},
{
name: "mcpserver with existing toolconfig",
mcpServer: &mcpv1alpha1.MCPServer{
ObjectMeta: metav1.ObjectMeta{
Name: "test-server",
Namespace: "default",
},
Spec: mcpv1alpha1.MCPServerSpec{
Image: "test-image",
ToolConfigRef: &mcpv1alpha1.ToolConfigRef{
Name: "test-config",
},
},
},
existingConfig: &mcpv1alpha1.MCPToolConfig{
ObjectMeta: metav1.ObjectMeta{
Name: "test-config",
Namespace: "default",
},
Spec: mcpv1alpha1.MCPToolConfigSpec{
ToolsFilter: []string{"tool1"},
},
},
expectConfig: true,
expectError: false,
},
{
name: "mcpserver with non-existent toolconfig",
mcpServer: &mcpv1alpha1.MCPServer{
ObjectMeta: metav1.ObjectMeta{
Name: "test-server",
Namespace: "default",
},
Spec: mcpv1alpha1.MCPServerSpec{
Image: "test-image",
ToolConfigRef: &mcpv1alpha1.ToolConfigRef{
Name: "non-existent",
},
},
},
expectConfig: false,
expectError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

ctx := t.Context()

scheme := runtime.NewScheme()
require.NoError(t, mcpv1alpha1.AddToScheme(scheme))

objs := []client.Object{}
if tt.existingConfig != nil {
objs = append(objs, tt.existingConfig)
}

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

config, err := GetToolConfigForMCPServer(ctx, fakeClient, tt.mcpServer)

if tt.expectError {
assert.Error(t, err)
assert.Nil(t, config)
} else {
assert.NoError(t, err)
if tt.expectConfig {
assert.NotNil(t, config)
assert.Equal(t, tt.existingConfig.Name, config.Name)
} else {
assert.Nil(t, config)
}
}
})
}
}
41 changes: 41 additions & 0 deletions cmd/thv-operator/pkg/controllerutil/tools_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package controllerutil

import (
"context"
"fmt"

"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"

mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1"
)

// GetToolConfigForMCPServer retrieves the MCPToolConfig referenced by an MCPServer
func GetToolConfigForMCPServer(
ctx context.Context,
c client.Client,
mcpServer *mcpv1alpha1.MCPServer,
) (*mcpv1alpha1.MCPToolConfig, error) {
if mcpServer.Spec.ToolConfigRef == nil {
// We throw an error because in this case you assume there is a ToolConfig
// but there isn't one referenced.
return nil, fmt.Errorf("MCPServer %s does not reference a MCPToolConfig", mcpServer.Name)
}

toolConfig := &mcpv1alpha1.MCPToolConfig{}
err := c.Get(ctx, types.NamespacedName{
Name: mcpServer.Spec.ToolConfigRef.Name,
Namespace: mcpServer.Namespace, // Same namespace as MCPServer
}, toolConfig)

if err != nil {
if errors.IsNotFound(err) {
return nil, fmt.Errorf("MCPToolConfig %s not found in namespace %s",
mcpServer.Spec.ToolConfigRef.Name, mcpServer.Namespace)
}
return nil, fmt.Errorf("failed to get MCPToolConfig: %w", err)
}

return toolConfig, nil
}
Loading
Loading