Skip to content

Commit 9ad87d3

Browse files
committed
feat(kubernetes): marshal all resources to yaml omitting managed fields
1 parent e1432e7 commit 9ad87d3

File tree

5 files changed

+145
-59
lines changed

5 files changed

+145
-59
lines changed

pkg/kubernetes/resources.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ package kubernetes
22

33
import (
44
"context"
5-
"encoding/json"
65
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
6+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
77
"k8s.io/apimachinery/pkg/runtime/schema"
88
"k8s.io/client-go/discovery"
99
memory "k8s.io/client-go/discovery/cached"
1010
"k8s.io/client-go/dynamic"
1111
"k8s.io/client-go/restmapper"
12+
"sigs.k8s.io/yaml"
1213
)
1314

1415
// TODO: WIP
@@ -29,7 +30,15 @@ func (k *Kubernetes) ResourcesList(ctx context.Context, gvk *schema.GroupVersion
2930
}
3031

3132
func marshal(v any) (string, error) {
32-
ret, err := json.Marshal(v)
33+
switch t := v.(type) {
34+
case []unstructured.Unstructured:
35+
for i := range t {
36+
t[i].SetManagedFields(nil)
37+
}
38+
case unstructured.Unstructured:
39+
t.SetManagedFields(nil)
40+
}
41+
ret, err := yaml.Marshal(v)
3342
if err != nil {
3443
return "", err
3544
}

pkg/mcp/common_test.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,11 @@ func (c *mcpContext) afterEach() {
6565
c.testServer.Close()
6666
}
6767

68-
func testCase(test func(t *testing.T, c *mcpContext)) func(*testing.T) {
69-
return func(t *testing.T) {
70-
mcpCtx := &mcpContext{}
71-
mcpCtx.beforeEach(t)
72-
defer mcpCtx.afterEach()
73-
test(t, mcpCtx)
74-
}
68+
func testCase(t *testing.T, test func(c *mcpContext)) {
69+
mcpCtx := &mcpContext{}
70+
mcpCtx.beforeEach(t)
71+
defer mcpCtx.afterEach()
72+
test(mcpCtx)
7573
}
7674

7775
func (c *mcpContext) withKubeConfig(rc *rest.Config) *api.Config {

pkg/mcp/configuration_test.go

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,78 @@ package mcp
22

33
import (
44
"github.com/mark3labs/mcp-go/mcp"
5-
"strings"
5+
v1 "k8s.io/client-go/tools/clientcmd/api/v1"
6+
"sigs.k8s.io/yaml"
67
"testing"
78
)
89

910
func TestConfigurationView(t *testing.T) {
10-
t.Run("configuration_view returns configuration", testCase(func(t *testing.T, c *mcpContext) {
11+
testCase(t, func(c *mcpContext) {
1112
configurationGet := mcp.CallToolRequest{}
1213
configurationGet.Params.Name = "configuration_view"
1314
configurationGet.Params.Arguments = map[string]interface{}{}
14-
tools, err := c.mcpClient.CallTool(c.ctx, configurationGet)
15-
if err != nil {
16-
t.Fatalf("call tool failed %v", err)
17-
return
18-
}
19-
resultContent := tools.Content[0].(map[string]interface{})["text"].(string)
20-
if !strings.Contains(resultContent, "cluster: fake\n") {
21-
t.Fatalf("mismatch in kube config: %s", resultContent)
22-
return
23-
}
24-
}))
15+
toolResult, err := c.mcpClient.CallTool(c.ctx, configurationGet)
16+
t.Run("configuration_view returns configuration", func(t *testing.T) {
17+
if err != nil {
18+
t.Fatalf("call tool failed %v", err)
19+
return
20+
}
21+
})
22+
var decoded *v1.Config
23+
err = yaml.Unmarshal([]byte(toolResult.Content[0].(map[string]interface{})["text"].(string)), &decoded)
24+
t.Run("configuration_view has yaml content", func(t *testing.T) {
25+
if err != nil {
26+
t.Fatalf("invalid tool result content %v", err)
27+
return
28+
}
29+
})
30+
t.Run("configuration_view returns current-context", func(t *testing.T) {
31+
if decoded.CurrentContext != "fake-context" {
32+
t.Fatalf("fake-context not found: %v", decoded.CurrentContext)
33+
return
34+
}
35+
})
36+
t.Run("configuration_view returns context info", func(t *testing.T) {
37+
if len(decoded.Contexts) != 1 {
38+
t.Fatalf("invalid context count, expected 1, got %v", len(decoded.Contexts))
39+
return
40+
}
41+
if decoded.Contexts[0].Name != "fake-context" {
42+
t.Fatalf("fake-context not found: %v", decoded.Contexts)
43+
return
44+
}
45+
if decoded.Contexts[0].Context.Cluster != "fake" {
46+
t.Fatalf("fake-cluster not found: %v", decoded.Contexts)
47+
return
48+
}
49+
if decoded.Contexts[0].Context.AuthInfo != "fake" {
50+
t.Fatalf("fake-auth not found: %v", decoded.Contexts)
51+
return
52+
}
53+
})
54+
t.Run("configuration_view returns cluster info", func(t *testing.T) {
55+
if len(decoded.Clusters) != 1 {
56+
t.Fatalf("invalid cluster count, expected 1, got %v", len(decoded.Clusters))
57+
return
58+
}
59+
if decoded.Clusters[0].Name != "fake" {
60+
t.Fatalf("fake-cluster not found: %v", decoded.Clusters)
61+
return
62+
}
63+
if decoded.Clusters[0].Cluster.Server != "https://example.com" {
64+
t.Fatalf("fake-server not found: %v", decoded.Clusters)
65+
return
66+
}
67+
})
68+
t.Run("configuration_view returns auth info", func(t *testing.T) {
69+
if len(decoded.AuthInfos) != 1 {
70+
t.Fatalf("invalid auth info count, expected 1, got %v", len(decoded.AuthInfos))
71+
return
72+
}
73+
if decoded.AuthInfos[0].Name != "fake" {
74+
t.Fatalf("fake-auth not found: %v", decoded.AuthInfos)
75+
return
76+
}
77+
})
78+
})
2579
}

pkg/mcp/mcp_test.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,25 @@ import (
77

88
func TestTools(t *testing.T) {
99
expectedNames := []string{"pods_list", "pods_list_in_namespace", "configuration_view"}
10-
t.Run("Has configuration_view tool", testCase(func(t *testing.T, c *mcpContext) {
10+
testCase(t, func(c *mcpContext) {
1111
tools, err := c.mcpClient.ListTools(c.ctx, mcp.ListToolsRequest{})
12+
t.Run("ListTools returns tools", func(t *testing.T) {
13+
if err != nil {
14+
t.Fatalf("call ListTools failed %v", err)
15+
return
16+
}
17+
})
1218
nameSet := make(map[string]bool)
1319
for _, tool := range tools.Tools {
1420
nameSet[tool.Name] = true
1521
}
1622
for _, name := range expectedNames {
17-
if nameSet[name] != true {
18-
t.Fatalf("tool name mismatch %v", err)
19-
return
20-
}
23+
t.Run("ListTools has "+name+" tool", func(t *testing.T) {
24+
if nameSet[name] != true {
25+
t.Fatalf("tool %s not found", name)
26+
return
27+
}
28+
})
2129
}
22-
}))
30+
})
2331
}

pkg/mcp/pods_test.go

Lines changed: 48 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,52 +2,69 @@ package mcp
22

33
import (
44
"context"
5-
"encoding/json"
65
"github.com/mark3labs/mcp-go/mcp"
76
corev1 "k8s.io/api/core/v1"
87
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
98
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
109
"k8s.io/client-go/kubernetes"
10+
"sigs.k8s.io/yaml"
1111
"testing"
1212
)
1313

1414
func TestPodsListInAllNamespaces(t *testing.T) {
15-
t.Run("pods_list", testCase(func(t *testing.T, c *mcpContext) {
15+
testCase(t, func(c *mcpContext) {
1616
createTestData(c.ctx, c.newKubernetesClient())
1717
configurationGet := mcp.CallToolRequest{}
1818
configurationGet.Params.Name = "pods_list"
1919
configurationGet.Params.Arguments = map[string]interface{}{}
2020
toolResult, err := c.mcpClient.CallTool(c.ctx, configurationGet)
21-
if err != nil {
22-
t.Fatalf("call tool failed %v", err)
23-
return
24-
}
21+
t.Run("pods_list returns pods list", func(t *testing.T) {
22+
if err != nil {
23+
t.Fatalf("call tool failed %v", err)
24+
return
25+
}
26+
})
2527
var decoded []unstructured.Unstructured
26-
if json.Unmarshal([]byte(toolResult.Content[0].(map[string]interface{})["text"].(string)), &decoded) != nil {
27-
t.Fatalf("invalid tool result content %v", err)
28-
return
29-
}
30-
if len(decoded) != 2 {
31-
t.Fatalf("invalid pods count, expected 2, got %v", len(decoded))
32-
return
33-
}
34-
if decoded[0].GetName() != "a-pod-in-ns-1" {
35-
t.Fatalf("invalid pod name, expected a-pod-in-ns-1, got %v", decoded[0].GetName())
36-
return
37-
}
38-
if decoded[0].GetNamespace() != "ns-1" {
39-
t.Fatalf("invalid pod namespace, expected ns-1, got %v", decoded[0].GetNamespace())
40-
return
41-
}
42-
if decoded[1].GetName() != "a-pod-in-ns-2" {
43-
t.Fatalf("invalid pod name, expected a-pod-in-ns-2, got %v", decoded[1].GetName())
44-
return
45-
}
46-
if decoded[1].GetNamespace() != "ns-2" {
47-
t.Fatalf("invalid pod namespace, expected ns-2, got %v", decoded[1].GetNamespace())
48-
return
49-
}
50-
}))
28+
err = yaml.Unmarshal([]byte(toolResult.Content[0].(map[string]interface{})["text"].(string)), &decoded)
29+
t.Run("pods_list has yaml content", func(t *testing.T) {
30+
if err != nil {
31+
t.Fatalf("invalid tool result content %v", err)
32+
return
33+
}
34+
})
35+
t.Run("pods_list returns 2 items", func(t *testing.T) {
36+
if len(decoded) != 2 {
37+
t.Fatalf("invalid pods count, expected 2, got %v", len(decoded))
38+
return
39+
}
40+
})
41+
t.Run("pods_list returns pod in ns-1", func(t *testing.T) {
42+
if decoded[0].GetName() != "a-pod-in-ns-1" {
43+
t.Fatalf("invalid pod name, expected a-pod-in-ns-1, got %v", decoded[0].GetName())
44+
return
45+
}
46+
if decoded[0].GetNamespace() != "ns-1" {
47+
t.Fatalf("invalid pod namespace, expected ns-1, got %v", decoded[0].GetNamespace())
48+
return
49+
}
50+
})
51+
t.Run("pods_list returns pod in ns-2", func(t *testing.T) {
52+
if decoded[1].GetName() != "a-pod-in-ns-2" {
53+
t.Fatalf("invalid pod name, expected a-pod-in-ns-2, got %v", decoded[1].GetName())
54+
return
55+
}
56+
if decoded[1].GetNamespace() != "ns-2" {
57+
t.Fatalf("invalid pod namespace, expected ns-2, got %v", decoded[1].GetNamespace())
58+
return
59+
}
60+
})
61+
t.Run("pods_list omits managed fields", func(t *testing.T) {
62+
if decoded[0].GetManagedFields() != nil {
63+
t.Fatalf("managed fields should be omitted, got %v", decoded[0].GetManagedFields())
64+
return
65+
}
66+
})
67+
})
5168
}
5269

5370
func createTestData(ctx context.Context, kc *kubernetes.Clientset) {

0 commit comments

Comments
 (0)