diff --git a/pkg/toolsets/netedge/coredns_test.go b/pkg/toolsets/netedge/coredns_test.go index 138b48490..e3b3a8245 100644 --- a/pkg/toolsets/netedge/coredns_test.go +++ b/pkg/toolsets/netedge/coredns_test.go @@ -1,12 +1,6 @@ package netedge import ( - "context" - "testing" - - "github.com/containers/kubernetes-mcp-server/pkg/api" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -15,17 +9,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" ) -// mockKubernetesClient implements api.KubernetesClient for testing -type mockKubernetesClient struct { - api.KubernetesClient - restConfig *rest.Config -} - -func (m *mockKubernetesClient) RESTConfig() *rest.Config { - return m.restConfig -} +func (s *NetEdgeTestSuite) TestGetCoreDNSConfig() { -func TestGetCoreDNSConfig(t *testing.T) { tests := []struct { name string configMap *corev1.ConfigMap @@ -72,7 +57,7 @@ func TestGetCoreDNSConfig(t *testing.T) { } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + s.Run(tt.name, func() { // Setup mock client objs := []runtime.Object{} if tt.configMap != nil { @@ -87,24 +72,19 @@ func TestGetCoreDNSConfig(t *testing.T) { return fake.NewClientBuilder().WithRuntimeObjects(objs...).Build(), nil } - // Call handler - params := api.ToolHandlerParams{ - Context: context.Background(), - KubernetesClient: &mockKubernetesClient{restConfig: &rest.Config{}}, - } - - result, err := getCoreDNSConfig(params) + // Call handler using suite params (which has valid RESTConfig from SetupTest) + result, err := getCoreDNSConfig(s.params) if tt.expectError { - require.NoError(t, err) // Handler returns error in result, not as return value - require.NotNil(t, result) - require.Error(t, result.Error) - assert.Contains(t, result.Error.Error(), tt.errorContains) + s.Require().NoError(err) // Handler returns error in result, not as return value + s.Require().NotNil(result) + s.Require().Error(result.Error) + s.Assert().Contains(result.Error.Error(), tt.errorContains) } else { - require.NoError(t, err) - require.NotNil(t, result) - require.NoError(t, result.Error) - assert.Equal(t, tt.expectedOutput, result.Content) + s.Require().NoError(err) + s.Require().NotNil(result) + s.Require().NoError(result.Error) + s.Assert().Equal(tt.expectedOutput, result.Content) } }) } diff --git a/pkg/toolsets/netedge/mock_test.go b/pkg/toolsets/netedge/mock_test.go new file mode 100644 index 000000000..9d2c8490d --- /dev/null +++ b/pkg/toolsets/netedge/mock_test.go @@ -0,0 +1,34 @@ +package netedge + +import ( + "github.com/containers/kubernetes-mcp-server/pkg/api" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" +) + +// Mock implementations +type mockToolCallRequest struct { + args map[string]interface{} +} + +func (m *mockToolCallRequest) GetArguments() map[string]interface{} { + return m.args +} + +func (m *mockToolCallRequest) GetName() string { + return "mock_tool" +} + +type mockKubernetesClient struct { + api.KubernetesClient + restConfig *rest.Config + dynamicClient dynamic.Interface +} + +func (m *mockKubernetesClient) RESTConfig() *rest.Config { + return m.restConfig +} + +func (m *mockKubernetesClient) DynamicClient() dynamic.Interface { + return m.dynamicClient +} diff --git a/pkg/toolsets/netedge/routes.go b/pkg/toolsets/netedge/routes.go new file mode 100644 index 000000000..07b3549eb --- /dev/null +++ b/pkg/toolsets/netedge/routes.go @@ -0,0 +1,73 @@ +package netedge + +import ( + "encoding/json" + "fmt" + + "github.com/containers/kubernetes-mcp-server/pkg/api" + "github.com/google/jsonschema-go/jsonschema" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/utils/ptr" +) + +func initRoutes() []api.ServerTool { + return []api.ServerTool{ + { + Tool: api.Tool{ + Name: "inspect_route", + Description: "Inspect an OpenShift Route to view its full configuration and status.", + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "namespace": { + Type: "string", + Description: "Route namespace", + }, + "route": { + Type: "string", + Description: "Route name", + }, + }, + Required: []string{"namespace", "route"}, + }, + Annotations: api.ToolAnnotations{ + Title: "Inspect Route", + ReadOnlyHint: ptr.To(true), + DestructiveHint: ptr.To(false), + OpenWorldHint: ptr.To(true), + }, + }, + Handler: inspectRoute, + }, + } +} + +func inspectRoute(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + namespace, err := api.RequiredString(params, "namespace") + if err != nil { + return api.NewToolCallResult("", err), nil + } + routeName, err := api.RequiredString(params, "route") + if err != nil { + return api.NewToolCallResult("", err), nil + } + + gvr := schema.GroupVersionResource{ + Group: "route.openshift.io", + Version: "v1", + Resource: "routes", + } + + route, err := params.DynamicClient().Resource(gvr).Namespace(namespace).Get(params.Context, routeName, metav1.GetOptions{}) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to get route %s/%s: %w", namespace, routeName, err)), nil + } + + data, err := json.MarshalIndent(route.Object, "", " ") + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to marshal route: %w", err)), nil + } + + return api.NewToolCallResult(string(data), nil), nil +} diff --git a/pkg/toolsets/netedge/routes_test.go b/pkg/toolsets/netedge/routes_test.go new file mode 100644 index 000000000..23f480d69 --- /dev/null +++ b/pkg/toolsets/netedge/routes_test.go @@ -0,0 +1,99 @@ +package netedge + +import ( + "encoding/json" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/dynamic/fake" +) + +func (s *NetEdgeTestSuite) TestInspectRoute() { + + tests := []struct { + name string + namespace string + route string + existingObjs []runtime.Object + expectedError string + validate func(result string) + }{ + { + name: "successful retrieval", + namespace: "default", + route: "my-route", + existingObjs: []runtime.Object{ + &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "route.openshift.io/v1", + "kind": "Route", + "metadata": map[string]interface{}{ + "name": "my-route", + "namespace": "default", + }, + "spec": map[string]interface{}{ + "host": "example.com", + }, + }, + }, + }, + validate: func(result string) { + var r map[string]interface{} + err := json.Unmarshal([]byte(result), &r) + s.Require().NoError(err) + s.Assert().Equal("my-route", r["metadata"].(map[string]interface{})["name"]) + s.Assert().Equal("example.com", r["spec"].(map[string]interface{})["host"]) + }, + }, + { + name: "route not found", + namespace: "default", + route: "missing", + existingObjs: []runtime.Object{}, + expectedError: "failed to get route", + }, + { + name: "missing arguments", + namespace: "", + route: "", + expectedError: "parameter required", + }, + } + + for _, tt := range tests { + s.Run(tt.name, func() { + // Create fake dynamic client + scheme := runtime.NewScheme() + dynClient := fake.NewSimpleDynamicClient(scheme, tt.existingObjs...) + + // Create mock params + args := make(map[string]any) + if tt.namespace != "" { + args["namespace"] = tt.namespace + } + if tt.route != "" { + args["route"] = tt.route + } + + // Set args using suite helper + s.SetArgs(args) + s.SetDynamicClient(dynClient) + + result, err := inspectRoute(s.params) + + if tt.expectedError != "" { + s.Assert().NoError(err) + s.Require().NotNil(result) + s.Require().Error(result.Error) + s.Assert().Contains(result.Error.Error(), tt.expectedError) + } else { + s.Assert().NoError(err) + s.Require().NotNil(result) + s.Assert().NoError(result.Error) + if tt.validate != nil { + tt.validate(result.Content) + } + } + }) + } +} diff --git a/pkg/toolsets/netedge/suite_test.go b/pkg/toolsets/netedge/suite_test.go new file mode 100644 index 000000000..a44bd893e --- /dev/null +++ b/pkg/toolsets/netedge/suite_test.go @@ -0,0 +1,42 @@ +package netedge + +import ( + "context" + "testing" + + "github.com/containers/kubernetes-mcp-server/pkg/api" + "github.com/stretchr/testify/suite" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" +) + +type NetEdgeTestSuite struct { + suite.Suite + params api.ToolHandlerParams + mockReq *mockToolCallRequest + mockClient *mockKubernetesClient +} + +func (s *NetEdgeTestSuite) SetupTest() { + s.mockReq = &mockToolCallRequest{args: make(map[string]interface{})} + s.mockClient = &mockKubernetesClient{ + restConfig: &rest.Config{}, + } + s.params = api.ToolHandlerParams{ + Context: context.Background(), + ToolCallRequest: s.mockReq, + KubernetesClient: s.mockClient, + } +} + +func (s *NetEdgeTestSuite) SetArgs(args map[string]interface{}) { + s.mockReq.args = args +} + +func (s *NetEdgeTestSuite) SetDynamicClient(dynClient dynamic.Interface) { + s.mockClient.dynamicClient = dynClient +} + +func TestNetEdgeSuite(t *testing.T) { + suite.Run(t, new(NetEdgeTestSuite)) +} diff --git a/pkg/toolsets/netedge/toolset.go b/pkg/toolsets/netedge/toolset.go index c3ed2c15a..ec52e389d 100644 --- a/pkg/toolsets/netedge/toolset.go +++ b/pkg/toolsets/netedge/toolset.go @@ -26,6 +26,7 @@ func (t *Toolset) GetTools(_ api.Openshift) []api.ServerTool { return slices.Concat( netedgeTools.InitQueryPrometheus(), initCoreDNS(), + initRoutes(), ) }