Skip to content
Merged
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
34 changes: 30 additions & 4 deletions pkg/k8s/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,32 +134,58 @@ func (c *Client) ListAPIResources(_ context.Context) ([]*metav1.APIResourceList,
}

// ListClusteredResources returns all clustered resources of the specified group/version/kind
// with optional pagination support via limit and continueToken parameters
func (c *Client) ListClusteredResources(
ctx context.Context,
gvr schema.GroupVersionResource,
labelSelector string,
limit int64,
continueToken string,
) (*unstructured.UnstructuredList, error) {
c.mu.RLock()
defer c.mu.RUnlock()

return c.dynamicClient.Resource(gvr).List(ctx, metav1.ListOptions{
listOptions := metav1.ListOptions{
LabelSelector: labelSelector,
})
}

// Add pagination options if provided
if limit > 0 {
listOptions.Limit = limit
}
if continueToken != "" {
listOptions.Continue = continueToken
}

return c.dynamicClient.Resource(gvr).List(ctx, listOptions)
}

// ListNamespacedResources returns all namespaced resources of the specified group/version/kind in the given namespace
// with optional pagination support via limit and continueToken parameters
func (c *Client) ListNamespacedResources(
ctx context.Context,
gvr schema.GroupVersionResource,
namespace string,
labelSelector string,
limit int64,
continueToken string,
) (*unstructured.UnstructuredList, error) {
c.mu.RLock()
defer c.mu.RUnlock()

return c.dynamicClient.Resource(gvr).Namespace(namespace).List(ctx, metav1.ListOptions{
listOptions := metav1.ListOptions{
LabelSelector: labelSelector,
})
}

// Add pagination options if provided
if limit > 0 {
listOptions.Limit = limit
}
if continueToken != "" {
listOptions.Continue = continueToken
}

return c.dynamicClient.Resource(gvr).Namespace(namespace).List(ctx, listOptions)
}

// ApplyClusteredResource creates or updates a clustered resource
Expand Down
125 changes: 121 additions & 4 deletions pkg/k8s/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func TestListClusteredResources(t *testing.T) {

// Test ListClusteredResources
ctx := context.Background()
list, err := testClient.ListClusteredResources(ctx, gvr, "")
list, err := testClient.ListClusteredResources(ctx, gvr, "", 0, "")

// Verify there was no error
assert.NoError(t, err, "ListClusteredResources should not return an error")
Expand All @@ -95,7 +95,7 @@ func TestListClusteredResources(t *testing.T) {
assert.Equal(t, "test-cluster-role", list.Items[0].GetName(), "Expected name 'test-cluster-role'")

// Test ListClusteredResources with label selector
list, err = testClient.ListClusteredResources(ctx, gvr, "app=test-app")
list, err = testClient.ListClusteredResources(ctx, gvr, "app=test-app", 0, "")

// Verify there was no error
assert.NoError(t, err, "ListClusteredResources should not return an error")
Expand Down Expand Up @@ -178,7 +178,7 @@ func TestListNamespacedResources(t *testing.T) {

// Test ListNamespacedResources
ctx := context.Background()
list, err := testClient.ListNamespacedResources(ctx, gvr, "default", "")
list, err := testClient.ListNamespacedResources(ctx, gvr, "default", "", 0, "")

// Verify there was no error
assert.NoError(t, err, "ListNamespacedResources should not return an error")
Expand All @@ -188,7 +188,7 @@ func TestListNamespacedResources(t *testing.T) {
assert.Equal(t, "test-service", list.Items[0].GetName(), "Expected name 'test-service'")

// Test ListNamespacedResources with label selector
list, err = testClient.ListNamespacedResources(ctx, gvr, "default", "app=test-app")
list, err = testClient.ListNamespacedResources(ctx, gvr, "default", "app=test-app", 0, "")

// Verify there was no error
assert.NoError(t, err, "ListNamespacedResources should not return an error")
Expand All @@ -198,6 +198,123 @@ func TestListNamespacedResources(t *testing.T) {
assert.Equal(t, "test-service-2", list.Items[0].GetName(), "Expected name 'test-service-2'")
}

// TestListClusteredResourcesWithPagination tests that pagination parameters are correctly handled
func TestListClusteredResourcesWithPagination(t *testing.T) {
// Since the fake client doesn't properly pass through ListOptions in the reactor,
// we'll test that our functions correctly build the ListOptions and that the
// response's continue token is properly preserved

scheme := runtime.NewScheme()
listKinds := map[schema.GroupVersionResource]string{
{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "clusterroles"}: "ClusterRoleList",
}

client := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds)
testClient := &Client{
dynamicClient: client,
}

gvr := schema.GroupVersionResource{
Group: "rbac.authorization.k8s.io",
Version: "v1",
Resource: "clusterroles",
}

// Add a reactor that returns a list with a continue token
client.PrependReactor("list", "clusterroles", func(_ ktesting.Action) (handled bool, ret runtime.Object, err error) {
list := &unstructured.UnstructuredList{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"continue": "test-continue-token",
},
},
Items: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "ClusterRole",
"metadata": map[string]interface{}{
"name": "test-role",
},
},
},
},
}
return true, list, nil
})

// Test that the function accepts pagination parameters and returns results
ctx := context.Background()

// Test with limit
list, err := testClient.ListClusteredResources(ctx, gvr, "", 10, "")
assert.NoError(t, err, "ListClusteredResources with limit should not return an error")
assert.NotNil(t, list, "List should not be nil")
assert.Equal(t, "test-continue-token", list.GetContinue(), "Continue token should be preserved in response")

// Test with continue token
list, err = testClient.ListClusteredResources(ctx, gvr, "", 0, "my-continue-token")
assert.NoError(t, err, "ListClusteredResources with continue token should not return an error")
assert.NotNil(t, list, "List should not be nil")
}

// TestListNamespacedResourcesWithPagination tests that pagination parameters are correctly handled
func TestListNamespacedResourcesWithPagination(t *testing.T) {
scheme := runtime.NewScheme()
listKinds := map[schema.GroupVersionResource]string{
{Group: "", Version: "v1", Resource: "pods"}: "PodList",
}

client := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds)
testClient := &Client{
dynamicClient: client,
}

gvr := schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "pods",
}

// Add a reactor that returns a list with a continue token
client.PrependReactor("list", "pods", func(_ ktesting.Action) (handled bool, ret runtime.Object, err error) {
list := &unstructured.UnstructuredList{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"continue": "pod-continue-token",
},
},
Items: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"name": "test-pod",
"namespace": "default",
},
},
},
},
}
return true, list, nil
})

// Test that the function accepts pagination parameters and returns results
ctx := context.Background()

// Test with limit
list, err := testClient.ListNamespacedResources(ctx, gvr, "default", "", 5, "")
assert.NoError(t, err, "ListNamespacedResources with limit should not return an error")
assert.NotNil(t, list, "List should not be nil")
assert.Equal(t, "pod-continue-token", list.GetContinue(), "Continue token should be preserved in response")

// Test with continue token
list, err = testClient.ListNamespacedResources(ctx, gvr, "default", "", 0, "pod-token")
assert.NoError(t, err, "ListNamespacedResources with continue token should not return an error")
assert.NotNil(t, list, "List should not be nil")
}

func TestApplyClusteredResource(t *testing.T) {
// Create a fake dynamic client
scheme := runtime.NewScheme()
Expand Down
11 changes: 8 additions & 3 deletions pkg/mcp/list_resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ func (m *Implementation) HandleListResources(ctx context.Context, request mcp.Ca
"exclude_annotation_keys", []string{"kubectl.kubernetes.io/last-applied-configuration"})
includeAnnotationKeys := request.GetStringSlice("include_annotation_keys", []string{})

// Parse pagination parameters
limit := request.GetInt("limit", 0) // 0 means no limit
continueToken := request.GetString("continue", "")

// Validate parameters
if resourceType == "" {
return mcp.NewToolResultError("resource_type is required"), nil
Expand Down Expand Up @@ -66,14 +70,14 @@ func (m *Implementation) HandleListResources(ctx context.Context, request mcp.Ca
Resource: resource,
}

// List resources
// List resources with pagination support
var list *unstructured.UnstructuredList
var err error
switch resourceType {
case types.ResourceTypeClustered:
list, err = m.k8sClient.ListClusteredResources(ctx, gvr, labelSelector)
list, err = m.k8sClient.ListClusteredResources(ctx, gvr, labelSelector, int64(limit), continueToken)
case types.ResourceTypeNamespaced:
list, err = m.k8sClient.ListNamespacedResources(ctx, gvr, namespace, labelSelector)
list, err = m.k8sClient.ListNamespacedResources(ctx, gvr, namespace, labelSelector, int64(limit), continueToken)
default:
return mcp.NewToolResultError(fmt.Sprintf("Invalid resource_type: %s", resourceType)), nil
}
Expand All @@ -90,6 +94,7 @@ func (m *Implementation) HandleListResources(ctx context.Context, request mcp.Ca
},
ListMeta: metav1.ListMeta{
ResourceVersion: list.GetResourceVersion(),
Continue: list.GetContinue(),
},
Items: make([]metav1.PartialObjectMetadata, 0, len(list.Items)),
}
Expand Down
Loading