Skip to main content
Glama

kubernetes-mcp

by kkb0318
describe_test.go12.3 kB
package tools import ( "context" "testing" "github.com/mark3labs/mcp-go/mcp" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/apimachinery/pkg/watch" ) type FakeDescribeResourceInterface struct { resource *unstructured.Unstructured } func (f *FakeDescribeResourceInterface) Create(ctx context.Context, obj *unstructured.Unstructured, options metav1.CreateOptions, subresources ...string) (*unstructured.Unstructured, error) { return nil, nil } func (f *FakeDescribeResourceInterface) Update(ctx context.Context, obj *unstructured.Unstructured, options metav1.UpdateOptions, subresources ...string) (*unstructured.Unstructured, error) { return nil, nil } func (f *FakeDescribeResourceInterface) UpdateStatus(ctx context.Context, obj *unstructured.Unstructured, options metav1.UpdateOptions) (*unstructured.Unstructured, error) { return nil, nil } func (f *FakeDescribeResourceInterface) Delete(ctx context.Context, name string, options metav1.DeleteOptions, subresources ...string) error { return nil } func (f *FakeDescribeResourceInterface) DeleteCollection(ctx context.Context, options metav1.DeleteOptions, listOptions metav1.ListOptions) error { return nil } func (f *FakeDescribeResourceInterface) Get(ctx context.Context, name string, options metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) { return f.resource, nil } func (f *FakeDescribeResourceInterface) List(ctx context.Context, opts metav1.ListOptions) (*unstructured.UnstructuredList, error) { return nil, nil } func (f *FakeDescribeResourceInterface) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { return nil, nil } func (f *FakeDescribeResourceInterface) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, options metav1.PatchOptions, subresources ...string) (*unstructured.Unstructured, error) { return nil, nil } func (f *FakeDescribeResourceInterface) Apply(ctx context.Context, name string, obj *unstructured.Unstructured, options metav1.ApplyOptions, subresources ...string) (*unstructured.Unstructured, error) { return nil, nil } func (f *FakeDescribeResourceInterface) ApplyStatus(ctx context.Context, name string, obj *unstructured.Unstructured, options metav1.ApplyOptions) (*unstructured.Unstructured, error) { return nil, nil } type FakeDescribeKubernetesClient struct { resource *unstructured.Unstructured } func (f FakeDescribeKubernetesClient) DynamicClient() (dynamic.Interface, error) { return nil, nil } func (f FakeDescribeKubernetesClient) DiscoClient() (discovery.DiscoveryInterface, error) { fakeDisco := &fakeDiscoveryClient{ apiResourceLists: []*metav1.APIResourceList{ { GroupVersion: "apps/v1", APIResources: []metav1.APIResource{ {Kind: "Deployment", Name: "deployments", Namespaced: true}, }, }, { GroupVersion: "v1", APIResources: []metav1.APIResource{ {Kind: "Pod", Name: "pods", Namespaced: true}, }, }, }, } return fakeDisco, nil } func (f FakeDescribeKubernetesClient) Clientset() (*kubernetes.Clientset, error) { return nil, nil } func (f FakeDescribeKubernetesClient) RESTMapper() (meta.RESTMapper, error) { return nil, nil } func (f FakeDescribeKubernetesClient) ResourceInterface(gvr schema.GroupVersionResource, namespaced bool, ns string) (dynamic.ResourceInterface, error) { return &FakeDescribeResourceInterface{resource: f.resource}, nil } func TestDescribeTool_Tool(t *testing.T) { client := FakeDescribeKubernetesClient{} multiClient := NewFakeMultiClusterClient(client) tool := NewDescribeTool(multiClient) mcpTool := tool.Tool() assert.Equal(t, "describe_resource", mcpTool.Name) assert.Contains(t, mcpTool.Description, "Describe a specific Kubernetes resource") } func TestDescribeTool_Handler(t *testing.T) { testPod := &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "v1", "kind": "Pod", "metadata": map[string]interface{}{ "name": "test-pod", "namespace": "default", "labels": map[string]interface{}{ "app": "test", }, "annotations": map[string]interface{}{ "kubernetes.io/change-cause": "test deployment", }, "creationTimestamp": "2023-01-01T00:00:00Z", "resourceVersion": "12345", "uid": "test-uid-123", }, "spec": map[string]interface{}{ "containers": []interface{}{ map[string]interface{}{ "name": "test-container", "image": "nginx:latest", }, }, }, "status": map[string]interface{}{ "phase": "Running", "conditions": []interface{}{ map[string]interface{}{ "type": "Ready", "status": "True", }, }, }, }, } testCases := []struct { name string client Client request map[string]any expectError bool }{ { name: "SuccessfulDescribe", client: FakeDescribeKubernetesClient{resource: testPod}, request: map[string]any{ "kind": "Pod", "name": "test-pod", "namespace": "default", }, expectError: false, }, { name: "MissingKind", client: FakeDescribeKubernetesClient{resource: testPod}, request: map[string]any{ "name": "test-pod", "namespace": "default", }, expectError: true, }, { name: "MissingName", client: FakeDescribeKubernetesClient{resource: testPod}, request: map[string]any{ "kind": "Pod", "namespace": "default", }, expectError: true, }, { name: "EmptyKind", client: FakeDescribeKubernetesClient{resource: testPod}, request: map[string]any{ "kind": "", "name": "test-pod", "namespace": "default", }, expectError: true, }, { name: "EmptyName", client: FakeDescribeKubernetesClient{resource: testPod}, request: map[string]any{ "kind": "Pod", "name": "", "namespace": "default", }, expectError: true, }, } for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { multiClient := NewFakeMultiClusterClient(tt.client) tool := NewDescribeTool(multiClient) req := &mcp.CallToolRequest{} req.Params.Arguments = tt.request result, err := tool.Handler(context.TODO(), *req) if tt.expectError { assert.Error(t, err) assert.Nil(t, result) } else { assert.NoError(t, err) assert.NotNil(t, result) assert.NotEmpty(t, result.Content) textContent, ok := result.Content[0].(mcp.TextContent) assert.True(t, ok) assert.Equal(t, "text", textContent.Type) } }) } } func TestParseAndValidateDescribeParams(t *testing.T) { testCases := []struct { name string args map[string]any expected *DescribeResourceInput expectedErr bool }{ { name: "ValidMinimal", args: map[string]any{ "kind": "Pod", "name": "test-pod", }, expected: &DescribeResourceInput{ Kind: "Pod", Name: "test-pod", Namespace: metav1.NamespaceAll, }, expectedErr: false, }, { name: "ValidWithNamespace", args: map[string]any{ "kind": "Deployment", "name": "test-deployment", "namespace": "default", }, expected: &DescribeResourceInput{ Kind: "Deployment", Name: "test-deployment", Namespace: "default", }, expectedErr: false, }, { name: "MissingKind", args: map[string]any{ "name": "test-pod", "namespace": "default", }, expected: nil, expectedErr: true, }, { name: "MissingName", args: map[string]any{ "kind": "Pod", "namespace": "default", }, expected: nil, expectedErr: true, }, { name: "EmptyKind", args: map[string]any{ "kind": "", "name": "test-pod", "namespace": "default", }, expected: nil, expectedErr: true, }, { name: "EmptyName", args: map[string]any{ "kind": "Pod", "name": "", "namespace": "default", }, expected: nil, expectedErr: true, }, { name: "EmptyNamespace", args: map[string]any{ "kind": "Pod", "name": "test-pod", "namespace": "", }, expected: &DescribeResourceInput{ Kind: "Pod", Name: "test-pod", Namespace: metav1.NamespaceAll, }, expectedErr: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result, err := parseAndValidateDescribeParams(tc.args) if tc.expectedErr { assert.Error(t, err) assert.Nil(t, result) } else { assert.NoError(t, err) assert.Equal(t, tc.expected, result) } }) } } func TestDescribeTool_FormatResourceDescription(t *testing.T) { client := FakeDescribeKubernetesClient{} multiClient := NewFakeMultiClusterClient(client) tool := NewDescribeTool(multiClient) testPod := &unstructured.Unstructured{} testPod.SetName("test-pod") testPod.SetNamespace("default") testPod.SetKind("Pod") testPod.SetLabels(map[string]string{ "app": "test", }) testPod.SetAnnotations(map[string]string{ "kubernetes.io/change-cause": "test deployment", }) testPod.SetUID("test-uid-123") testPod.SetResourceVersion("12345") // Set spec and status using the Object field testPod.Object = map[string]interface{}{ "metadata": map[string]interface{}{ "name": "test-pod", "namespace": "default", "labels": map[string]interface{}{ "app": "test", }, "annotations": map[string]interface{}{ "kubernetes.io/change-cause": "test deployment", }, "uid": "test-uid-123", "resourceVersion": "12345", }, "kind": "Pod", "spec": map[string]interface{}{ "containers": []interface{}{ map[string]interface{}{ "name": "test-container", "image": "nginx:latest", }, }, }, "status": map[string]interface{}{ "phase": "Running", "conditions": []interface{}{ map[string]interface{}{ "type": "Ready", "status": "True", }, }, }, } result := tool.formatResourceDescription(testPod) assert.Equal(t, "test-pod", result["name"]) assert.Equal(t, "default", result["namespace"]) assert.Equal(t, "Pod", result["kind"]) assert.NotNil(t, result["labels"]) assert.NotNil(t, result["annotations"]) assert.Equal(t, "test-uid-123", string(result["uid"].(types.UID))) assert.Equal(t, "12345", result["resourceVersion"]) assert.NotNil(t, result["spec"]) assert.NotNil(t, result["status"]) // Check spec content spec, ok := result["spec"].(map[string]interface{}) assert.True(t, ok) containers, ok := spec["containers"].([]interface{}) assert.True(t, ok) assert.Len(t, containers, 1) // Check status content status, ok := result["status"].(map[string]interface{}) assert.True(t, ok) assert.Equal(t, "Running", status["phase"]) assert.NotNil(t, status["conditions"]) } func TestDescribeTool_FormatResourceDescriptionWithoutSpecStatus(t *testing.T) { client := FakeDescribeKubernetesClient{} multiClient := NewFakeMultiClusterClient(client) tool := NewDescribeTool(multiClient) // Create a resource without spec and status testConfigMap := &unstructured.Unstructured{} testConfigMap.SetName("test-configmap") testConfigMap.SetNamespace("default") testConfigMap.SetKind("ConfigMap") testConfigMap.SetUID("configmap-uid-123") testConfigMap.Object = map[string]interface{}{ "metadata": map[string]interface{}{ "name": "test-configmap", "namespace": "default", "uid": "configmap-uid-123", }, "kind": "ConfigMap", "data": map[string]interface{}{ "key1": "value1", "key2": "value2", }, } result := tool.formatResourceDescription(testConfigMap) assert.Equal(t, "test-configmap", result["name"]) assert.Equal(t, "default", result["namespace"]) assert.Equal(t, "ConfigMap", result["kind"]) assert.Equal(t, "configmap-uid-123", string(result["uid"].(types.UID))) // spec and status should not be present since they don't exist in the resource _, specExists := result["spec"] _, statusExists := result["status"] assert.False(t, specExists) assert.False(t, statusExists) }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kkb0318/kubernetes-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server