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
44 changes: 12 additions & 32 deletions pkg/toolsets/netedge/coredns_test.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -15,17 +9,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we do grouping via testify/suite ?
e.g. similar to requested here: #131 (comment) ?

I guess we can also do that in a follow up PR, for all netedge tests.


// 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
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}
})
}
Expand Down
34 changes: 34 additions & 0 deletions pkg/toolsets/netedge/mock_test.go
Original file line number Diff line number Diff line change
@@ -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
}
73 changes: 73 additions & 0 deletions pkg/toolsets/netedge/routes.go
Original file line number Diff line number Diff line change
@@ -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
}
99 changes: 99 additions & 0 deletions pkg/toolsets/netedge/routes_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
})
}
}
42 changes: 42 additions & 0 deletions pkg/toolsets/netedge/suite_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
1 change: 1 addition & 0 deletions pkg/toolsets/netedge/toolset.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func (t *Toolset) GetTools(_ api.Openshift) []api.ServerTool {
return slices.Concat(
netedgeTools.InitQueryPrometheus(),
initCoreDNS(),
initRoutes(),
)
}

Expand Down