diff --git a/pkg/toolsets/netedge/coredns_test.go b/pkg/toolsets/netedge/coredns_test.go index 35acdd15d..5d02e537e 100644 --- a/pkg/toolsets/netedge/coredns_test.go +++ b/pkg/toolsets/netedge/coredns_test.go @@ -9,7 +9,6 @@ import ( ) func (s *NetEdgeTestSuite) TestGetCoreDNSConfig() { - tests := []struct { name string configMap *corev1.ConfigMap diff --git a/pkg/toolsets/netedge/routes.go b/pkg/toolsets/netedge/routes.go new file mode 100644 index 000000000..0a0805af3 --- /dev/null +++ b/pkg/toolsets/netedge/routes.go @@ -0,0 +1,104 @@ +package netedge + +import ( + "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/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/utils/ptr" + "sigs.k8s.io/yaml" +) + +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 + } + + keyFields := map[string]interface{}{ + "Name": route.GetName(), + "Namespace": route.GetNamespace(), + } + + if host, found, err := unstructured.NestedString(route.Object, "spec", "host"); found && err == nil { + keyFields["Host"] = host + } + + if tls, found, err := unstructured.NestedMap(route.Object, "spec", "tls"); found && err == nil { + keyFields["TLS"] = tls + } + + if to, found, err := unstructured.NestedMap(route.Object, "spec", "to"); found && err == nil { + keyFields["To"] = to + } + + if port, found, err := unstructured.NestedMap(route.Object, "spec", "port"); found && err == nil { + keyFields["Port"] = port + } + + if ingress, found, err := unstructured.NestedSlice(route.Object, "status", "ingress"); found && err == nil { + keyFields["IngressStatus"] = ingress + } + + resultObj := map[string]interface{}{ + "KeyFields": keyFields, + "RawRoute": route.Object, + } + + data, err := yaml.Marshal(resultObj) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to marshal route as yaml: %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..f6b90973d --- /dev/null +++ b/pkg/toolsets/netedge/routes_test.go @@ -0,0 +1,104 @@ +package netedge + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/dynamic/fake" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/yaml" +) + +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 := yaml.Unmarshal([]byte(result), &r) + s.Require().NoError(err) + rawRoute := r["RawRoute"].(map[string]interface{}) + keyFields := r["KeyFields"].(map[string]interface{}) + s.Assert().Equal("my-route", rawRoute["metadata"].(map[string]interface{})["name"]) + s.Assert().Equal("example.com", rawRoute["spec"].(map[string]interface{})["host"]) + s.Assert().Equal("my-route", keyFields["Name"]) + s.Assert().Equal("default", keyFields["Namespace"]) + s.Assert().Equal("example.com", keyFields["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() + err := clientgoscheme.AddToScheme(scheme) + s.Require().NoError(err) + 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 + } + + 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/toolset.go b/pkg/toolsets/netedge/toolset.go index 006733fa2..76ac80f18 100644 --- a/pkg/toolsets/netedge/toolset.go +++ b/pkg/toolsets/netedge/toolset.go @@ -27,6 +27,7 @@ func (t *Toolset) GetTools(_ api.Openshift) []api.ServerTool { initCoreDNS(), initEndpoints(), initProbeDNSLocal(), + initRoutes(), ) }