Skip to content

Commit f2c5c6d

Browse files
Merge pull request #2 from ShahryarShabani/feature/search-resources
feat: Add filtering to search.Resources tool
2 parents 6b5f419 + ea7f525 commit f2c5c6d

File tree

5 files changed

+159
-59
lines changed

5 files changed

+159
-59
lines changed

pkg/kubernetes/search.go

Lines changed: 95 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -12,71 +12,49 @@ import (
1212
"k8s.io/apimachinery/pkg/runtime/schema"
1313
)
1414

15-
// SearchResources searches for a query string in all resources across all namespaces.
16-
func (k *Kubernetes) SearchResources(ctx context.Context, query string, asTable bool) (runtime.Unstructured, error) {
17-
// Discovery client is used to discover different supported API groups, versions and resources.
18-
serverResources, err := k.manager.discoveryClient.ServerPreferredResources()
19-
if err != nil {
20-
return nil, fmt.Errorf("failed to get server resources: %w", err)
21-
}
22-
15+
// SearchResources searches for a query string in resources, with optional filters for resource type and namespace.
16+
func (k *Kubernetes) SearchResources(ctx context.Context, query, apiVersion, kind, namespaceLabelSelector string, asTable bool) (runtime.Unstructured, error) {
2317
var matchingResources []unstructured.Unstructured
24-
for _, apiResourceList := range serverResources {
25-
for _, apiResource := range apiResourceList.APIResources {
26-
// Skip resources that do not support the "list" verb
27-
if !contains(apiResource.Verbs, "list") {
28-
continue
29-
}
3018

31-
gvk := schema.GroupVersionKind{
32-
Group: apiResourceList.GroupVersion,
33-
Version: apiResource.Version,
34-
Kind: apiResource.Kind,
35-
}
36-
if gvk.Group == "" {
37-
gvk.Group = "core"
38-
}
19+
if apiVersion != "" && kind != "" {
20+
// Search in a specific resource type
21+
gv, err := schema.ParseGroupVersion(apiVersion)
22+
if err != nil {
23+
return nil, fmt.Errorf("invalid apiVersion: %w", err)
24+
}
25+
gvk := gv.WithKind(kind)
26+
apiResource, err := k.getAPIResource(&gvk)
27+
if err != nil {
28+
return nil, fmt.Errorf("failed to get API resource: %w", err)
29+
}
30+
resources, err := k.searchInGVK(ctx, query, &gvk, apiResource, namespaceLabelSelector)
31+
if err != nil {
32+
return nil, err
33+
}
34+
matchingResources = append(matchingResources, resources...)
35+
} else {
36+
// Search in all resources
37+
serverResources, err := k.manager.discoveryClient.ServerPreferredResources()
38+
if err != nil {
39+
return nil, fmt.Errorf("failed to get server resources: %w", err)
40+
}
3941

40-
if _, err := k.resourceFor(&gvk); err != nil {
41-
// Ignore errors for resources that cannot be mapped
42-
continue
43-
}
44-
var namespaces []string
45-
if apiResource.Namespaced {
46-
// Get all namespaces
47-
nsListObj, err := k.NamespacesList(ctx, ResourceListOptions{})
48-
if err != nil {
49-
return nil, fmt.Errorf("failed to list namespaces: %w", err)
42+
for _, apiResourceList := range serverResources {
43+
for _, apiResource := range apiResourceList.APIResources {
44+
gvk := schema.GroupVersionKind{
45+
Group: apiResourceList.GroupVersion,
46+
Version: apiResource.Version,
47+
Kind: apiResource.Kind,
5048
}
51-
if unstructuredList, ok := nsListObj.(*unstructured.UnstructuredList); ok {
52-
for _, ns := range unstructuredList.Items {
53-
namespaces = append(namespaces, ns.GetName())
54-
}
49+
if gvk.Group == "" {
50+
gvk.Group = "core"
5551
}
56-
} else {
57-
// For cluster-scoped resources, use an empty namespace
58-
namespaces = append(namespaces, "")
59-
}
60-
61-
for _, ns := range namespaces {
62-
list, err := k.ResourcesList(ctx, &gvk, ns, ResourceListOptions{})
52+
resources, err := k.searchInGVK(ctx, query, &gvk, &apiResource, namespaceLabelSelector)
6353
if err != nil {
64-
// Ignore errors for resources that cannot be listed
54+
// Ignore errors for resources that cannot be searched
6555
continue
6656
}
67-
68-
if unstructuredList, ok := list.(*unstructured.UnstructuredList); ok {
69-
for _, item := range unstructuredList.Items {
70-
match, err := matchResource(item, query)
71-
if err != nil {
72-
// Ignore errors during matching
73-
continue
74-
}
75-
if match {
76-
matchingResources = append(matchingResources, item)
77-
}
78-
}
79-
}
57+
matchingResources = append(matchingResources, resources...)
8058
}
8159
}
8260
}
@@ -94,6 +72,65 @@ func (k *Kubernetes) SearchResources(ctx context.Context, query string, asTable
9472
}, nil
9573
}
9674

75+
func (k *Kubernetes) searchInGVK(ctx context.Context, query string, gvk *schema.GroupVersionKind, apiResource *metav1.APIResource, namespaceLabelSelector string) ([]unstructured.Unstructured, error) {
76+
if !contains(apiResource.Verbs, "list") {
77+
return nil, nil // Skip resources that do not support the "list" verb
78+
}
79+
80+
var matchingResources []unstructured.Unstructured
81+
var namespaces []string
82+
if apiResource.Namespaced {
83+
nsListOptions := ResourceListOptions{}
84+
if namespaceLabelSelector != "" {
85+
nsListOptions.LabelSelector = namespaceLabelSelector
86+
}
87+
nsListObj, err := k.NamespacesList(ctx, nsListOptions)
88+
if err != nil {
89+
return nil, fmt.Errorf("failed to list namespaces: %w", err)
90+
}
91+
if unstructuredList, ok := nsListObj.(*unstructured.UnstructuredList); ok {
92+
for _, ns := range unstructuredList.Items {
93+
namespaces = append(namespaces, ns.GetName())
94+
}
95+
}
96+
} else {
97+
namespaces = append(namespaces, "") // For cluster-scoped resources
98+
}
99+
100+
for _, ns := range namespaces {
101+
list, err := k.ResourcesList(ctx, gvk, ns, ResourceListOptions{})
102+
if err != nil {
103+
continue // Ignore errors for resources that cannot be listed
104+
}
105+
106+
if unstructuredList, ok := list.(*unstructured.UnstructuredList); ok {
107+
for _, item := range unstructuredList.Items {
108+
match, err := matchResource(item, query)
109+
if err != nil {
110+
continue // Ignore errors during matching
111+
}
112+
if match {
113+
matchingResources = append(matchingResources, item)
114+
}
115+
}
116+
}
117+
}
118+
return matchingResources, nil
119+
}
120+
121+
func (k *Kubernetes) getAPIResource(gvk *schema.GroupVersionKind) (*metav1.APIResource, error) {
122+
apiResourceList, err := k.manager.discoveryClient.ServerResourcesForGroupVersion(gvk.GroupVersion().String())
123+
if err != nil {
124+
return nil, err
125+
}
126+
for _, apiResource := range apiResourceList.APIResources {
127+
if apiResource.Kind == gvk.Kind {
128+
return &apiResource, nil
129+
}
130+
}
131+
return nil, fmt.Errorf("resource not found for GVK: %s", gvk)
132+
}
133+
97134
func (k *Kubernetes) createTable(resources []unstructured.Unstructured) (runtime.Unstructured, error) {
98135
table := &metav1.Table{
99136
TypeMeta: metav1.TypeMeta{
@@ -141,4 +178,4 @@ func contains(slice []string, s string) bool {
141178
}
142179
}
143180
return false
144-
}
181+
}

pkg/mcp/testdata/toolsets-core-tools.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,18 @@
434434
"as_table": {
435435
"description": "Return the results as a table.",
436436
"type": "boolean"
437+
},
438+
"api_version": {
439+
"description": "Optional API version of the resource to search in.",
440+
"type": "string"
441+
},
442+
"kind": {
443+
"description": "Optional kind of the resource to search in.",
444+
"type": "string"
445+
},
446+
"namespace_label_selector": {
447+
"description": "Optional label selector to filter namespaces.",
448+
"type": "string"
437449
}
438450
},
439451
"required": [

pkg/mcp/testdata/toolsets-full-tools-openshift.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,18 @@
554554
"as_table": {
555555
"description": "Return the results as a table.",
556556
"type": "boolean"
557+
},
558+
"api_version": {
559+
"description": "Optional API version of the resource to search in.",
560+
"type": "string"
561+
},
562+
"kind": {
563+
"description": "Optional kind of the resource to search in.",
564+
"type": "string"
565+
},
566+
"namespace_label_selector": {
567+
"description": "Optional label selector to filter namespaces.",
568+
"type": "string"
557569
}
558570
},
559571
"required": [

pkg/mcp/testdata/toolsets-full-tools.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,18 @@
540540
"as_table": {
541541
"description": "Return the results as a table.",
542542
"type": "boolean"
543+
},
544+
"api_version": {
545+
"description": "Optional API version of the resource to search in.",
546+
"type": "string"
547+
},
548+
"kind": {
549+
"description": "Optional kind of the resource to search in.",
550+
"type": "string"
551+
},
552+
"namespace_label_selector": {
553+
"description": "Optional label selector to filter namespaces.",
554+
"type": "string"
543555
}
544556
},
545557
"required": [

pkg/toolsets/core/search.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@ func initSearch(_ kubernetes.Openshift) []api.ServerTool {
2727
Type: "boolean",
2828
Description: "Return the results as a table.",
2929
},
30+
"api_version": {
31+
Type: "string",
32+
Description: "Optional API version of the resource to search in.",
33+
},
34+
"kind": {
35+
Type: "string",
36+
Description: "Optional kind of the resource to search in.",
37+
},
38+
"namespace_label_selector": {
39+
Type: "string",
40+
Description: "Optional label selector to filter namespaces.",
41+
},
3042
},
3143
Required: []string{"query"},
3244
},
@@ -50,7 +62,22 @@ func searchResources(params api.ToolHandlerParams) (*api.ToolCallResult, error)
5062
asTable = val
5163
}
5264

53-
result, err := params.SearchResources(params, query, asTable)
65+
apiVersion := ""
66+
if val, ok := params.GetArguments()["api_version"].(string); ok {
67+
apiVersion = val
68+
}
69+
70+
kind := ""
71+
if val, ok := params.GetArguments()["kind"].(string); ok {
72+
kind = val
73+
}
74+
75+
namespaceLabelSelector := ""
76+
if val, ok := params.GetArguments()["namespace_label_selector"].(string); ok {
77+
namespaceLabelSelector = val
78+
}
79+
80+
result, err := params.SearchResources(params, query, apiVersion, kind, namespaceLabelSelector, asTable)
5481
if err != nil {
5582
return api.NewToolCallResult("", fmt.Errorf("failed to search resources: %v", err)), nil
5683
}

0 commit comments

Comments
 (0)