diff --git a/internal/content/json.go b/internal/content/json.go index 698d006..9bccaf7 100644 --- a/internal/content/json.go +++ b/internal/content/json.go @@ -6,7 +6,7 @@ import ( "github.com/strowk/foxy-contexts/pkg/mcp" ) -func NewJsonContent(v interface{}) (mcp.TextContent, error) { +func NewJsonContent(v any) (mcp.TextContent, error) { contents, err := json.Marshal(v) if err != nil { return mcp.TextContent{}, err diff --git a/internal/k8s/apps/v1/deployment/get_deployment.go b/internal/k8s/apps/v1/deployment/get_deployment.go index c801469..e464917 100644 --- a/internal/k8s/apps/v1/deployment/get_deployment.go +++ b/internal/k8s/apps/v1/deployment/get_deployment.go @@ -2,6 +2,9 @@ package deployment import ( "context" + "encoding/json" + "html/template" + "strings" "github.com/strowk/foxy-contexts/pkg/mcp" "github.com/strowk/mcp-k8s-go/internal/content" @@ -11,20 +14,59 @@ import ( "k8s.io/client-go/kubernetes" ) -func GetDeployment(clientset kubernetes.Interface, namespace string, name string) *mcp.CallToolResult { +func GetDeployment( + clientset kubernetes.Interface, + namespace string, + name string, + templateStr string, +) *mcp.CallToolResult { deployment, err := clientset.AppsV1().Deployments(namespace).Get(context.Background(), name, metav1.GetOptions{}) if err != nil { return utils.ErrResponse(err) } - utils.SanitizeObjectMeta(&deployment.ObjectMeta) - c, err := content.NewJsonContent(deployment) - if err != nil { - return utils.ErrResponse(err) + var cnt interface{} + if templateStr != "" { + tmpl, err := template.New("template").Parse(templateStr) + if err != nil { + return utils.ErrResponse(err) + } + + // transforming deployment to JSON and back so that it has clear structure + parsedDeployment, err := json.Marshal(deployment) + if err != nil { + return utils.ErrResponse(err) + } + + var parsedDeploymentMap map[string]interface{} + err = json.Unmarshal(parsedDeployment, &parsedDeploymentMap) + if err != nil { + return utils.ErrResponse(err) + } + + buf := new(strings.Builder) + + err = tmpl.Execute(buf, parsedDeploymentMap) + if err != nil { + return utils.ErrResponse(err) + } + + cnt = mcp.TextContent{ + Type: "text", + Text: buf.String(), + } + } else { + utils.SanitizeObjectMeta(&deployment.ObjectMeta) + c, err := content.NewJsonContent(deployment) + if err != nil { + return utils.ErrResponse(err) + } + cnt = c } + return &mcp.CallToolResult{ Meta: map[string]interface{}{}, - Content: []interface{}{c}, + Content: []interface{}{cnt}, IsError: utils.Ptr(false), } } diff --git a/internal/k8s/apps/v1/deployment/get_deployment_test.yaml b/internal/k8s/apps/v1/deployment/get_deployment_test.yaml index c2afe13..a9c3882 100644 --- a/internal/k8s/apps/v1/deployment/get_deployment_test.yaml +++ b/internal/k8s/apps/v1/deployment/get_deployment_test.yaml @@ -38,3 +38,34 @@ out: "isError": false, }, } + +--- +case: Get k8s deployment name +in: + { + "jsonrpc": "2.0", + "method": "tools/call", + "id": 2, + "params": + { + "name": "get-k8s-resource", + "arguments": + { + "context": "k3d-mcp-k8s-integration-test", + "namespace": "test-deployment", + "kind": "deployment", + "name": "nginx-deployment", + "go_template": "{{ .metadata.name }}", + }, + }, + } +out: + { + "jsonrpc": "2.0", + "id": 2, + "result": + { + "content": [{ "type": "text", "text": "nginx-deployment" }], + "isError": false, + }, + } diff --git a/internal/k8s/core/v1/pod/get_pod_test.yaml b/internal/k8s/core/v1/pod/get_pod_test.yaml index 4345778..bb65eee 100644 --- a/internal/k8s/core/v1/pod/get_pod_test.yaml +++ b/internal/k8s/core/v1/pod/get_pod_test.yaml @@ -32,3 +32,31 @@ out: "isError": false, }, } + +--- +case: Get k8s pod dnsPolicy +in: + { + "jsonrpc": "2.0", + "method": "tools/call", + "id": 2, + "params": + { + "name": "get-k8s-resource", + "arguments": + { + "context": "k3d-mcp-k8s-integration-test", + "namespace": "test", + "kind": "pod", + "name": "nginx", + "go_template": "{{ .spec.dnsPolicy }}", + }, + }, + } +out: + { + "jsonrpc": "2.0", + "id": 2, + "result": + { "content": [{ "type": "text", "text": "ClusterFirst" }], "isError": false }, + } diff --git a/internal/tools/get_resource_tool.go b/internal/tools/get_resource_tool.go index 0fe48b4..ed1c918 100644 --- a/internal/tools/get_resource_tool.go +++ b/internal/tools/get_resource_tool.go @@ -3,6 +3,7 @@ package tools import ( "context" "fmt" + "html/template" "strings" "github.com/strowk/foxy-contexts/pkg/fxctx" @@ -26,6 +27,7 @@ func NewGetResourceTool(pool k8s.ClientPool) fxctx.Tool { groupProperty := "group" versionProperty := "version" nameProperty := "name" + templateProperty := "go_template" inputSchema := toolinput.NewToolInputSchema( toolinput.WithString(contextProperty, "Name of the Kubernetes context to use, defaults to current context"), @@ -34,6 +36,7 @@ func NewGetResourceTool(pool k8s.ClientPool) fxctx.Tool { toolinput.WithString(versionProperty, "API Version of the resource to get"), toolinput.WithRequiredString(kindProperty, "Kind of resource to get"), toolinput.WithRequiredString(nameProperty, "Name of the resource to get"), + toolinput.WithString(templateProperty, "Go template to render the output, if not specified, the complete JSON object will be returned"), ) return fxctx.NewTool( @@ -72,8 +75,10 @@ func NewGetResourceTool(pool k8s.ClientPool) fxctx.Tool { return utils.ErrResponse(err) } + templateStr := input.StringOr(templateProperty, "") + if strings.ToLower(kind) == "deployment" && (group == "apps" || group == "") && (version == "v1" || version == "") { - return deployment.GetDeployment(clientset, namespace, name) + return deployment.GetDeployment(clientset, namespace, name, templateStr) } res, err := clientset.Discovery().ServerPreferredResources() @@ -120,11 +125,29 @@ func NewGetResourceTool(pool k8s.ClientPool) fxctx.Tool { } } - cnt, err := content.NewJsonContent(unstructured.Object) - if err != nil { - return utils.ErrResponse(err) + var cnt any + if templateStr != "" { + tmpl, err := template.New("template").Parse(templateStr) + if err != nil { + return utils.ErrResponse(err) + } + buf := new(strings.Builder) + err = tmpl.Execute(buf, object) + if err != nil { + return utils.ErrResponse(err) + } + cnt = mcp.TextContent{ + Type: "text", + Text: buf.String(), + } + } else { + c, err := content.NewJsonContent(object) + if err != nil { + return utils.ErrResponse(err) + } + cnt = c } - var contents = []interface{}{cnt} + var contents = []any{cnt} return &mcp.CallToolResult{ Meta: map[string]interface{}{},