mirror of
https://github.com/containers/kubernetes-mcp-server.git
synced 2025-10-23 01:22:57 +03:00
feat(kubernetes): marshal all resources to yaml omitting managed fields
This commit is contained in:
@@ -2,13 +2,14 @@ package kubernetes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/discovery"
|
||||
memory "k8s.io/client-go/discovery/cached"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/restmapper"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// TODO: WIP
|
||||
@@ -29,7 +30,15 @@ func (k *Kubernetes) ResourcesList(ctx context.Context, gvk *schema.GroupVersion
|
||||
}
|
||||
|
||||
func marshal(v any) (string, error) {
|
||||
ret, err := json.Marshal(v)
|
||||
switch t := v.(type) {
|
||||
case []unstructured.Unstructured:
|
||||
for i := range t {
|
||||
t[i].SetManagedFields(nil)
|
||||
}
|
||||
case unstructured.Unstructured:
|
||||
t.SetManagedFields(nil)
|
||||
}
|
||||
ret, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -65,13 +65,11 @@ func (c *mcpContext) afterEach() {
|
||||
c.testServer.Close()
|
||||
}
|
||||
|
||||
func testCase(test func(t *testing.T, c *mcpContext)) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
mcpCtx := &mcpContext{}
|
||||
mcpCtx.beforeEach(t)
|
||||
defer mcpCtx.afterEach()
|
||||
test(t, mcpCtx)
|
||||
}
|
||||
func testCase(t *testing.T, test func(c *mcpContext)) {
|
||||
mcpCtx := &mcpContext{}
|
||||
mcpCtx.beforeEach(t)
|
||||
defer mcpCtx.afterEach()
|
||||
test(mcpCtx)
|
||||
}
|
||||
|
||||
func (c *mcpContext) withKubeConfig(rc *rest.Config) *api.Config {
|
||||
|
||||
@@ -2,24 +2,78 @@ package mcp
|
||||
|
||||
import (
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"strings"
|
||||
v1 "k8s.io/client-go/tools/clientcmd/api/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigurationView(t *testing.T) {
|
||||
t.Run("configuration_view returns configuration", testCase(func(t *testing.T, c *mcpContext) {
|
||||
testCase(t, func(c *mcpContext) {
|
||||
configurationGet := mcp.CallToolRequest{}
|
||||
configurationGet.Params.Name = "configuration_view"
|
||||
configurationGet.Params.Arguments = map[string]interface{}{}
|
||||
tools, err := c.mcpClient.CallTool(c.ctx, configurationGet)
|
||||
if err != nil {
|
||||
t.Fatalf("call tool failed %v", err)
|
||||
return
|
||||
}
|
||||
resultContent := tools.Content[0].(map[string]interface{})["text"].(string)
|
||||
if !strings.Contains(resultContent, "cluster: fake\n") {
|
||||
t.Fatalf("mismatch in kube config: %s", resultContent)
|
||||
return
|
||||
}
|
||||
}))
|
||||
toolResult, err := c.mcpClient.CallTool(c.ctx, configurationGet)
|
||||
t.Run("configuration_view returns configuration", func(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("call tool failed %v", err)
|
||||
return
|
||||
}
|
||||
})
|
||||
var decoded *v1.Config
|
||||
err = yaml.Unmarshal([]byte(toolResult.Content[0].(map[string]interface{})["text"].(string)), &decoded)
|
||||
t.Run("configuration_view has yaml content", func(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("invalid tool result content %v", err)
|
||||
return
|
||||
}
|
||||
})
|
||||
t.Run("configuration_view returns current-context", func(t *testing.T) {
|
||||
if decoded.CurrentContext != "fake-context" {
|
||||
t.Fatalf("fake-context not found: %v", decoded.CurrentContext)
|
||||
return
|
||||
}
|
||||
})
|
||||
t.Run("configuration_view returns context info", func(t *testing.T) {
|
||||
if len(decoded.Contexts) != 1 {
|
||||
t.Fatalf("invalid context count, expected 1, got %v", len(decoded.Contexts))
|
||||
return
|
||||
}
|
||||
if decoded.Contexts[0].Name != "fake-context" {
|
||||
t.Fatalf("fake-context not found: %v", decoded.Contexts)
|
||||
return
|
||||
}
|
||||
if decoded.Contexts[0].Context.Cluster != "fake" {
|
||||
t.Fatalf("fake-cluster not found: %v", decoded.Contexts)
|
||||
return
|
||||
}
|
||||
if decoded.Contexts[0].Context.AuthInfo != "fake" {
|
||||
t.Fatalf("fake-auth not found: %v", decoded.Contexts)
|
||||
return
|
||||
}
|
||||
})
|
||||
t.Run("configuration_view returns cluster info", func(t *testing.T) {
|
||||
if len(decoded.Clusters) != 1 {
|
||||
t.Fatalf("invalid cluster count, expected 1, got %v", len(decoded.Clusters))
|
||||
return
|
||||
}
|
||||
if decoded.Clusters[0].Name != "fake" {
|
||||
t.Fatalf("fake-cluster not found: %v", decoded.Clusters)
|
||||
return
|
||||
}
|
||||
if decoded.Clusters[0].Cluster.Server != "https://example.com" {
|
||||
t.Fatalf("fake-server not found: %v", decoded.Clusters)
|
||||
return
|
||||
}
|
||||
})
|
||||
t.Run("configuration_view returns auth info", func(t *testing.T) {
|
||||
if len(decoded.AuthInfos) != 1 {
|
||||
t.Fatalf("invalid auth info count, expected 1, got %v", len(decoded.AuthInfos))
|
||||
return
|
||||
}
|
||||
if decoded.AuthInfos[0].Name != "fake" {
|
||||
t.Fatalf("fake-auth not found: %v", decoded.AuthInfos)
|
||||
return
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,17 +7,25 @@ import (
|
||||
|
||||
func TestTools(t *testing.T) {
|
||||
expectedNames := []string{"pods_list", "pods_list_in_namespace", "configuration_view"}
|
||||
t.Run("Has configuration_view tool", testCase(func(t *testing.T, c *mcpContext) {
|
||||
testCase(t, func(c *mcpContext) {
|
||||
tools, err := c.mcpClient.ListTools(c.ctx, mcp.ListToolsRequest{})
|
||||
t.Run("ListTools returns tools", func(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("call ListTools failed %v", err)
|
||||
return
|
||||
}
|
||||
})
|
||||
nameSet := make(map[string]bool)
|
||||
for _, tool := range tools.Tools {
|
||||
nameSet[tool.Name] = true
|
||||
}
|
||||
for _, name := range expectedNames {
|
||||
if nameSet[name] != true {
|
||||
t.Fatalf("tool name mismatch %v", err)
|
||||
return
|
||||
}
|
||||
t.Run("ListTools has "+name+" tool", func(t *testing.T) {
|
||||
if nameSet[name] != true {
|
||||
t.Fatalf("tool %s not found", name)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,52 +2,69 @@ package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"sigs.k8s.io/yaml"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPodsListInAllNamespaces(t *testing.T) {
|
||||
t.Run("pods_list", testCase(func(t *testing.T, c *mcpContext) {
|
||||
testCase(t, func(c *mcpContext) {
|
||||
createTestData(c.ctx, c.newKubernetesClient())
|
||||
configurationGet := mcp.CallToolRequest{}
|
||||
configurationGet.Params.Name = "pods_list"
|
||||
configurationGet.Params.Arguments = map[string]interface{}{}
|
||||
toolResult, err := c.mcpClient.CallTool(c.ctx, configurationGet)
|
||||
if err != nil {
|
||||
t.Fatalf("call tool failed %v", err)
|
||||
return
|
||||
}
|
||||
t.Run("pods_list returns pods list", func(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("call tool failed %v", err)
|
||||
return
|
||||
}
|
||||
})
|
||||
var decoded []unstructured.Unstructured
|
||||
if json.Unmarshal([]byte(toolResult.Content[0].(map[string]interface{})["text"].(string)), &decoded) != nil {
|
||||
t.Fatalf("invalid tool result content %v", err)
|
||||
return
|
||||
}
|
||||
if len(decoded) != 2 {
|
||||
t.Fatalf("invalid pods count, expected 2, got %v", len(decoded))
|
||||
return
|
||||
}
|
||||
if decoded[0].GetName() != "a-pod-in-ns-1" {
|
||||
t.Fatalf("invalid pod name, expected a-pod-in-ns-1, got %v", decoded[0].GetName())
|
||||
return
|
||||
}
|
||||
if decoded[0].GetNamespace() != "ns-1" {
|
||||
t.Fatalf("invalid pod namespace, expected ns-1, got %v", decoded[0].GetNamespace())
|
||||
return
|
||||
}
|
||||
if decoded[1].GetName() != "a-pod-in-ns-2" {
|
||||
t.Fatalf("invalid pod name, expected a-pod-in-ns-2, got %v", decoded[1].GetName())
|
||||
return
|
||||
}
|
||||
if decoded[1].GetNamespace() != "ns-2" {
|
||||
t.Fatalf("invalid pod namespace, expected ns-2, got %v", decoded[1].GetNamespace())
|
||||
return
|
||||
}
|
||||
}))
|
||||
err = yaml.Unmarshal([]byte(toolResult.Content[0].(map[string]interface{})["text"].(string)), &decoded)
|
||||
t.Run("pods_list has yaml content", func(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("invalid tool result content %v", err)
|
||||
return
|
||||
}
|
||||
})
|
||||
t.Run("pods_list returns 2 items", func(t *testing.T) {
|
||||
if len(decoded) != 2 {
|
||||
t.Fatalf("invalid pods count, expected 2, got %v", len(decoded))
|
||||
return
|
||||
}
|
||||
})
|
||||
t.Run("pods_list returns pod in ns-1", func(t *testing.T) {
|
||||
if decoded[0].GetName() != "a-pod-in-ns-1" {
|
||||
t.Fatalf("invalid pod name, expected a-pod-in-ns-1, got %v", decoded[0].GetName())
|
||||
return
|
||||
}
|
||||
if decoded[0].GetNamespace() != "ns-1" {
|
||||
t.Fatalf("invalid pod namespace, expected ns-1, got %v", decoded[0].GetNamespace())
|
||||
return
|
||||
}
|
||||
})
|
||||
t.Run("pods_list returns pod in ns-2", func(t *testing.T) {
|
||||
if decoded[1].GetName() != "a-pod-in-ns-2" {
|
||||
t.Fatalf("invalid pod name, expected a-pod-in-ns-2, got %v", decoded[1].GetName())
|
||||
return
|
||||
}
|
||||
if decoded[1].GetNamespace() != "ns-2" {
|
||||
t.Fatalf("invalid pod namespace, expected ns-2, got %v", decoded[1].GetNamespace())
|
||||
return
|
||||
}
|
||||
})
|
||||
t.Run("pods_list omits managed fields", func(t *testing.T) {
|
||||
if decoded[0].GetManagedFields() != nil {
|
||||
t.Fatalf("managed fields should be omitted, got %v", decoded[0].GetManagedFields())
|
||||
return
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func createTestData(ctx context.Context, kc *kubernetes.Clientset) {
|
||||
|
||||
Reference in New Issue
Block a user