Skip to main content
Glama

kubernetes-mcp

by kkb0318
list_events_test.go13.1 kB
package tools import ( "context" "encoding/json" "errors" "testing" "time" "github.com/mark3labs/mcp-go/mcp" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" ) type FakeEventsClient struct { clientset *kubernetes.Clientset err error } func (f *FakeEventsClient) Clientset() (*kubernetes.Clientset, error) { return f.clientset, f.err } func (f *FakeEventsClient) DynamicClient() (dynamic.Interface, error) { return nil, nil } func (f *FakeEventsClient) DiscoClient() (discovery.DiscoveryInterface, error) { return nil, nil } func (f *FakeEventsClient) RESTMapper() (meta.RESTMapper, error) { return nil, nil } func (f *FakeEventsClient) ResourceInterface(gvr schema.GroupVersionResource, namespaced bool, ns string) (dynamic.ResourceInterface, error) { return nil, nil } func TestListEventsTool_Handler_ClientsetError(t *testing.T) { client := &FakeEventsClient{ clientset: nil, err: errors.New("clientset error"), } multiClient := NewFakeMultiClusterClient(client) tool := NewListEventsTool(multiClient) req := mcp.CallToolRequest{ Params: struct { Name string `json:"name"` Arguments map[string]any `json:"arguments,omitempty"` Meta *struct { ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"` } `json:"_meta,omitempty"` }{ Arguments: map[string]any{ "namespace": "default", }, }, } actualResult, actualErr := tool.Handler(context.Background(), req) assert.Error(t, actualErr) assert.Contains(t, actualErr.Error(), "failed to get clientset: clientset error") assert.Nil(t, actualResult) } func TestParseAndValidateEventsParams(t *testing.T) { testCases := []struct { name string args map[string]any expectedErr bool validate func(*testing.T, *ListEventsInput) }{ { name: "ValidParams", args: map[string]any{ "namespace": "default", "object": "test-pod", "eventType": "Warning", "reason": "Failed", "since": "1h", "sinceTime": "2025-06-20T10:00:00Z", "limit": float64(50), "timeoutSeconds": float64(60), }, expectedErr: false, validate: func(t *testing.T, input *ListEventsInput) { assert.Equal(t, "default", input.Namespace) assert.Equal(t, "test-pod", input.Object) assert.Equal(t, "Warning", input.EventType) assert.Equal(t, "Failed", input.Reason) assert.Equal(t, "1h", input.Since) assert.Equal(t, "2025-06-20T10:00:00Z", input.SinceTime) assert.Equal(t, int64(50), input.Limit) assert.Equal(t, int64(60), input.TimeoutSeconds) }, }, { name: "MinimalParams", args: map[string]any{}, expectedErr: false, validate: func(t *testing.T, input *ListEventsInput) { assert.Empty(t, input.Namespace) assert.Empty(t, input.Object) assert.Empty(t, input.EventType) assert.Empty(t, input.Reason) assert.Empty(t, input.Since) assert.Empty(t, input.SinceTime) assert.Equal(t, int64(100), input.Limit) // Default value assert.Equal(t, int64(30), input.TimeoutSeconds) // Default value }, }, { name: "InvalidEventType", args: map[string]any{ "eventType": "Invalid", }, expectedErr: true, }, { name: "InvalidNamespace", args: map[string]any{ "namespace": "invalid-namespace-with-invalid-chars!", }, expectedErr: true, }, { name: "InvalidSinceDuration", args: map[string]any{ "since": "invalid-duration", }, expectedErr: true, }, { name: "InvalidSinceTime", args: map[string]any{ "sinceTime": "invalid-time-format", }, expectedErr: true, }, { name: "ValidEventTypeNormal", args: map[string]any{ "eventType": "normal", }, expectedErr: false, validate: func(t *testing.T, input *ListEventsInput) { assert.Equal(t, "normal", input.EventType) }, }, { name: "ValidEventTypeWarning", args: map[string]any{ "eventType": "WARNING", }, expectedErr: false, validate: func(t *testing.T, input *ListEventsInput) { assert.Equal(t, "WARNING", input.EventType) }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { client := &FakeEventsClient{} multiClient := NewFakeMultiClusterClient(client) tool := NewListEventsTool(multiClient) result, err := tool.parseAndValidateEventsParams(tc.args) if tc.expectedErr { assert.Error(t, err) assert.Nil(t, result) } else { assert.NoError(t, err) assert.NotNil(t, result) if tc.validate != nil { tc.validate(t, result) } } }) } } func TestListEventsTool_buildListOptions(t *testing.T) { client := &FakeEventsClient{} multiClient := NewFakeMultiClusterClient(client) tool := NewListEventsTool(multiClient) testCases := []struct { name string input *ListEventsInput validate func(*testing.T, metav1.ListOptions) }{ { name: "WithObject", input: &ListEventsInput{ Object: "test-pod", Limit: 50, TimeoutSeconds: 60, }, validate: func(t *testing.T, opts metav1.ListOptions) { assert.Equal(t, "involvedObject.name=test-pod", opts.FieldSelector) assert.Equal(t, int64(50), opts.Limit) assert.Equal(t, int64(60), *opts.TimeoutSeconds) }, }, { name: "DefaultValues", input: &ListEventsInput{}, validate: func(t *testing.T, opts metav1.ListOptions) { assert.Empty(t, opts.FieldSelector) assert.Equal(t, int64(0), opts.Limit) // No limit since input defaults weren't set assert.Equal(t, int64(0), *opts.TimeoutSeconds) // No timeout since input defaults weren't set }, }, { name: "ZeroLimit", input: &ListEventsInput{ Limit: 0, }, validate: func(t *testing.T, opts metav1.ListOptions) { assert.Equal(t, int64(0), opts.Limit) // No default applied in buildListOptions }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { opts := tool.buildListOptions(tc.input) tc.validate(t, opts) }) } } func TestListEventsTool_filterEvents(t *testing.T) { client := &FakeEventsClient{} multiClient := NewFakeMultiClusterClient(client) tool := NewListEventsTool(multiClient) // Create test events now := time.Now() oneHourAgo := now.Add(-1 * time.Hour) twoHoursAgo := now.Add(-2 * time.Hour) events := []corev1.Event{ { Type: "Normal", Reason: "Pulled", LastTimestamp: metav1.Time{Time: oneHourAgo}, }, { Type: "Warning", Reason: "Failed", LastTimestamp: metav1.Time{Time: twoHoursAgo}, }, { Type: "Warning", Reason: "FailedScheduling", LastTimestamp: metav1.Time{Time: oneHourAgo}, }, } testCases := []struct { name string input *ListEventsInput expected int }{ { name: "NoFilter", input: &ListEventsInput{}, expected: 3, }, { name: "FilterByEventType", input: &ListEventsInput{ EventType: "Warning", }, expected: 2, }, { name: "FilterByReason", input: &ListEventsInput{ Reason: "Failed", }, expected: 2, // Both "Failed" and "FailedScheduling" should match }, { name: "FilterByExactReason", input: &ListEventsInput{ Reason: "Pulled", }, expected: 1, }, { name: "FilterBySince", input: &ListEventsInput{ Since: "90m", // 1.5 hours }, expected: 2, // Only events from 1 hour ago }, { name: "CombinedFilters", input: &ListEventsInput{ EventType: "Warning", Since: "90m", }, expected: 1, // Only Warning events from 1 hour ago }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { filtered := tool.filterEvents(events, tc.input) assert.Len(t, filtered, tc.expected) }) } } func TestListEventsTool_isEventWithinTimeRange(t *testing.T) { client := &FakeEventsClient{} multiClient := NewFakeMultiClusterClient(client) tool := NewListEventsTool(multiClient) now := time.Now() event := &corev1.Event{ LastTimestamp: metav1.Time{Time: now.Add(-30 * time.Minute)}, } testCases := []struct { name string input *ListEventsInput expected bool }{ { name: "NoTimeFilter", input: &ListEventsInput{}, expected: true, }, { name: "WithinSinceRange", input: &ListEventsInput{ Since: "1h", }, expected: true, }, { name: "OutsideSinceRange", input: &ListEventsInput{ Since: "15m", }, expected: false, }, { name: "WithinSinceTimeRange", input: &ListEventsInput{ SinceTime: now.Add(-45 * time.Minute).Format(time.RFC3339), }, expected: true, }, { name: "OutsideSinceTimeRange", input: &ListEventsInput{ SinceTime: now.Add(-15 * time.Minute).Format(time.RFC3339), }, expected: false, }, { name: "InvalidSinceTime", input: &ListEventsInput{ SinceTime: "invalid-time", }, expected: true, // Should include all events if parsing fails }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result := tool.isEventWithinTimeRange(event, tc.input) assert.Equal(t, tc.expected, result) }) } } func TestListEventsTool_convertToEventInfos(t *testing.T) { client := &FakeEventsClient{} multiClient := NewFakeMultiClusterClient(client) tool := NewListEventsTool(multiClient) now := time.Now() events := []corev1.Event{ { ObjectMeta: metav1.ObjectMeta{ Namespace: "default", }, FirstTimestamp: metav1.Time{Time: now.Add(-1 * time.Hour)}, LastTimestamp: metav1.Time{Time: now.Add(-30 * time.Minute)}, Count: 3, Type: "Warning", Reason: "Failed", Message: "Container failed to start", InvolvedObject: corev1.ObjectReference{ Kind: "Pod", Name: "test-pod", Namespace: "default", }, Source: corev1.EventSource{ Component: "kubelet", Host: "node1", }, }, { ObjectMeta: metav1.ObjectMeta{ Namespace: "default", }, FirstTimestamp: metav1.Time{Time: now.Add(-2 * time.Hour)}, LastTimestamp: metav1.Time{Time: now.Add(-1 * time.Hour)}, Count: 1, Type: "Normal", Reason: "Pulled", Message: "Container image pulled", InvolvedObject: corev1.ObjectReference{ Kind: "Pod", Name: "test-pod", }, Source: corev1.EventSource{ Component: "kubelet", }, }, } eventInfos := tool.convertToEventInfos(events) assert.Len(t, eventInfos, 2) // Check first event assert.Equal(t, int32(3), eventInfos[0].Count) assert.Equal(t, "Warning", eventInfos[0].Type) assert.Equal(t, "Failed", eventInfos[0].Reason) assert.Equal(t, "Container failed to start", eventInfos[0].Message) assert.Equal(t, "default", eventInfos[0].Namespace) assert.Equal(t, "Pod/test-pod", eventInfos[0].Object) assert.Equal(t, "kubelet (node1)", eventInfos[0].Source) // Check second event assert.Equal(t, int32(1), eventInfos[1].Count) assert.Equal(t, "Normal", eventInfos[1].Type) assert.Equal(t, "Pulled", eventInfos[1].Reason) assert.Equal(t, "Container image pulled", eventInfos[1].Message) assert.Equal(t, "Pod/test-pod", eventInfos[1].Object) assert.Equal(t, "kubelet", eventInfos[1].Source) } func TestListEventsTool_Tool(t *testing.T) { client := &FakeEventsClient{} multiClient := NewFakeMultiClusterClient(client) tool := NewListEventsTool(multiClient) mcpTool := tool.Tool() assert.Equal(t, "list_events", mcpTool.Name) assert.Contains(t, mcpTool.Description, "List Kubernetes events") // Verify the tool has an input schema assert.NotNil(t, mcpTool.InputSchema) // Check that the tool schema has properties assert.NotNil(t, mcpTool.InputSchema.Properties) assert.True(t, len(mcpTool.InputSchema.Properties) > 0, "Tool should have input parameters") } func TestEventInfoJSONSerialization(t *testing.T) { now := time.Now() eventInfo := EventInfo{ FirstTimestamp: metav1.Time{Time: now.Add(-1 * time.Hour)}, LastTimestamp: metav1.Time{Time: now}, Count: 5, Type: "Warning", Reason: "Failed", Object: "Pod/test-pod", Message: "Container failed to start", Source: "kubelet (node1)", Namespace: "default", } // Test JSON serialization jsonData, err := json.Marshal(eventInfo) assert.NoError(t, err) assert.NotEmpty(t, jsonData) // Test JSON deserialization var deserializedEventInfo EventInfo err = json.Unmarshal(jsonData, &deserializedEventInfo) assert.NoError(t, err) assert.Equal(t, eventInfo.Count, deserializedEventInfo.Count) assert.Equal(t, eventInfo.Type, deserializedEventInfo.Type) assert.Equal(t, eventInfo.Reason, deserializedEventInfo.Reason) assert.Equal(t, eventInfo.Object, deserializedEventInfo.Object) assert.Equal(t, eventInfo.Message, deserializedEventInfo.Message) assert.Equal(t, eventInfo.Source, deserializedEventInfo.Source) assert.Equal(t, eventInfo.Namespace, deserializedEventInfo.Namespace) }

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