Skip to content

Commit f591e2b

Browse files
committed
feat(kubernetes): pods_get for explicit or nil namespace
1 parent 0f12797 commit f591e2b

File tree

7 files changed

+215
-51
lines changed

7 files changed

+215
-51
lines changed

pkg/kubernetes/configuration.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,23 @@
11
package kubernetes
22

33
import (
4-
"k8s.io/client-go/tools/clientcmd"
54
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
65
"k8s.io/client-go/tools/clientcmd/api/latest"
76
)
87

98
func ConfigurationView() (string, error) {
109
// TODO: consider in cluster run mode (current approach only shows kubeconfig)
11-
pathOptions := clientcmd.NewDefaultPathOptions()
12-
cfg, err := pathOptions.GetStartingConfig()
10+
cfg, err := resolveConfig().RawConfig()
1311
if err != nil {
1412
return "", err
1513
}
16-
if err = clientcmdapi.MinifyConfig(cfg); err != nil {
14+
if err = clientcmdapi.MinifyConfig(&cfg); err != nil {
1715
return "", err
1816
}
19-
if err = clientcmdapi.FlattenConfig(cfg); err != nil {
17+
if err = clientcmdapi.FlattenConfig(&cfg); err != nil {
2018
return "", err
2119
}
22-
convertedObj, err := latest.Scheme.ConvertToVersion(cfg, latest.ExternalVersion)
20+
convertedObj, err := latest.Scheme.ConvertToVersion(&cfg, latest.ExternalVersion)
2321
if err != nil {
2422
return "", err
2523
}

pkg/kubernetes/kubernetes.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"k8s.io/client-go/rest"
66
"k8s.io/client-go/restmapper"
77
"k8s.io/client-go/tools/clientcmd"
8+
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
89
"sigs.k8s.io/yaml"
910
)
1011

@@ -37,11 +38,28 @@ func marshal(v any) (string, error) {
3738
return string(ret), nil
3839
}
3940

41+
func resolveConfig() clientcmd.ClientConfig {
42+
pathOptions := clientcmd.NewDefaultPathOptions()
43+
//cfg, err := pathOptions.GetStartingConfig()
44+
//if err != nil {
45+
// return nil, err
46+
//}
47+
//if err = clientcmdapi.MinifyConfig(cfg); err != nil {
48+
// return nil, err
49+
//}
50+
//if err = clientcmdapi.FlattenConfig(cfg); err != nil {
51+
// return nil, err
52+
//}
53+
//return cfg, nil
54+
return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
55+
&clientcmd.ClientConfigLoadingRules{ExplicitPath: pathOptions.GetDefaultFilename()},
56+
&clientcmd.ConfigOverrides{ClusterInfo: clientcmdapi.Cluster{Server: ""}})
57+
}
58+
4059
func resolveClientConfig() (*rest.Config, error) {
4160
inClusterConfig, err := rest.InClusterConfig()
4261
if err == nil && inClusterConfig != nil {
4362
return inClusterConfig, nil
4463
}
45-
pathOptions := clientcmd.NewDefaultPathOptions()
46-
return clientcmd.BuildConfigFromFlags("", pathOptions.GetDefaultFilename())
64+
return resolveConfig().ClientConfig()
4765
}

pkg/kubernetes/pods.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,14 @@ func (k *Kubernetes) PodsListInNamespace(ctx context.Context, namespace string)
1616
Group: "", Version: "v1", Kind: "Pod",
1717
}, namespace)
1818
}
19+
20+
func (k *Kubernetes) PodsGet(ctx context.Context, namespace, name string) (string, error) {
21+
if namespace == "" {
22+
if ns, _, nsErr := resolveConfig().Namespace(); nsErr == nil {
23+
namespace = ns
24+
}
25+
}
26+
return k.ResourcesGet(ctx, &schema.GroupVersionKind{
27+
Group: "", Version: "v1", Kind: "Pod",
28+
}, namespace, name)
29+
}

pkg/kubernetes/resources.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,22 @@ func (k *Kubernetes) ResourcesList(ctx context.Context, gvk *schema.GroupVersion
2727
return marshal(rl.Items)
2828
}
2929

30+
func (k *Kubernetes) ResourcesGet(ctx context.Context, gvk *schema.GroupVersionKind, namespace, name string) (string, error) {
31+
client, err := dynamic.NewForConfig(k.cfg)
32+
if err != nil {
33+
return "", err
34+
}
35+
gvr, err := k.resourceFor(gvk)
36+
if err != nil {
37+
return "", err
38+
}
39+
rg, err := client.Resource(*gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
40+
if err != nil {
41+
return "", err
42+
}
43+
return marshal(rg)
44+
}
45+
3046
func (k *Kubernetes) resourceFor(gvk *schema.GroupVersionKind) (*schema.GroupVersionResource, error) {
3147
if k.deferredDiscoveryRESTMapper == nil {
3248
d, err := discovery.NewDiscoveryClientForConfig(k.cfg)

pkg/mcp/common_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"github.com/mark3labs/mcp-go/mcp"
77
"github.com/mark3labs/mcp-go/server"
88
"github.com/spf13/afero"
9+
corev1 "k8s.io/api/core/v1"
10+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
911
"k8s.io/client-go/kubernetes"
1012
"k8s.io/client-go/rest"
1113
"k8s.io/client-go/tools/clientcmd"
@@ -55,6 +57,8 @@ func TestMain(m *testing.M) {
5557
BinaryAssetsDirectory: filepath.Join(envTestDir, "k8s", versionDir),
5658
}
5759
envTestRestConfig, _ = envTest.Start()
60+
kc, _ := kubernetes.NewForConfig(envTestRestConfig)
61+
createTestData(context.Background(), kc)
5862

5963
// Test!
6064
code := m.Run()
@@ -111,6 +115,7 @@ func testCase(t *testing.T, test func(c *mcpContext)) {
111115
test(mcpCtx)
112116
}
113117

118+
// withKubeConfig sets up a fake kubeconfig in the temp directory based on the provided rest.Config
114119
func (c *mcpContext) withKubeConfig(rc *rest.Config) *api.Config {
115120
fakeConfig := api.NewConfig()
116121
fakeConfig.CurrentContext = "fake-context"
@@ -132,10 +137,12 @@ func (c *mcpContext) withKubeConfig(rc *rest.Config) *api.Config {
132137
return fakeConfig
133138
}
134139

140+
// withEnvTest sets up the environment for kubeconfig to be used with envTest
135141
func (c *mcpContext) withEnvTest() {
136142
c.withKubeConfig(envTestRestConfig)
137143
}
138144

145+
// newKubernetesClient creates a new Kubernetes client with the current kubeconfig
139146
func (c *mcpContext) newKubernetesClient() *kubernetes.Clientset {
140147
c.withEnvTest()
141148
pathOptions := clientcmd.NewDefaultPathOptions()
@@ -147,9 +154,44 @@ func (c *mcpContext) newKubernetesClient() *kubernetes.Clientset {
147154
return kubernetesClient
148155
}
149156

157+
// callTool helper function to call a tool by name with arguments
150158
func (c *mcpContext) callTool(name string, args map[string]interface{}) (*mcp.CallToolResult, error) {
151159
callToolRequest := mcp.CallToolRequest{}
152160
callToolRequest.Params.Name = name
153161
callToolRequest.Params.Arguments = args
154162
return c.mcpClient.CallTool(c.ctx, callToolRequest)
155163
}
164+
165+
func createTestData(ctx context.Context, kc *kubernetes.Clientset) {
166+
_, _ = kc.CoreV1().Namespaces().
167+
Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns-1"}}, metav1.CreateOptions{})
168+
_, _ = kc.CoreV1().Namespaces().
169+
Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns-2"}}, metav1.CreateOptions{})
170+
_, _ = kc.CoreV1().Pods("default").
171+
Create(ctx, &corev1.Pod{
172+
ObjectMeta: metav1.ObjectMeta{Name: "a-pod-in-default"},
173+
Spec: corev1.PodSpec{
174+
Containers: []corev1.Container{
175+
{Name: "nginx", Image: "nginx"},
176+
},
177+
},
178+
}, metav1.CreateOptions{})
179+
_, _ = kc.CoreV1().Pods("ns-1").
180+
Create(ctx, &corev1.Pod{
181+
ObjectMeta: metav1.ObjectMeta{Name: "a-pod-in-ns-1"},
182+
Spec: corev1.PodSpec{
183+
Containers: []corev1.Container{
184+
{Name: "nginx", Image: "nginx"},
185+
},
186+
},
187+
}, metav1.CreateOptions{})
188+
_, _ = kc.CoreV1().Pods("ns-2").
189+
Create(ctx, &corev1.Pod{
190+
ObjectMeta: metav1.ObjectMeta{Name: "a-pod-in-ns-2"},
191+
Spec: corev1.PodSpec{
192+
Containers: []corev1.Container{
193+
{Name: "nginx", Image: "nginx"},
194+
},
195+
},
196+
}, metav1.CreateOptions{})
197+
}

pkg/mcp/pods.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@ func (s *Sever) initPods() {
2121
mcp.Required(),
2222
),
2323
), podsListInNamespace)
24+
s.server.AddTool(mcp.NewTool(
25+
"pods_get",
26+
mcp.WithDescription("Get a Kubernetes Pod in the current namespace with the provided name"),
27+
mcp.WithString("namespace",
28+
mcp.Description("Namespace to get the Pod from"),
29+
),
30+
mcp.WithString("name",
31+
mcp.Description("Name of the Pod"),
32+
mcp.Required(),
33+
),
34+
), podsGet)
2435
}
2536

2637
func podsListInAllNamespaces(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
@@ -50,3 +61,23 @@ func podsListInNamespace(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.Cal
5061
}
5162
return NewTextResult(ret, err), nil
5263
}
64+
65+
func podsGet(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
66+
k, err := kubernetes.NewKubernetes()
67+
if err != nil {
68+
return NewTextResult("", fmt.Errorf("failed to get pod: %v", err)), nil
69+
}
70+
ns := ctr.Params.Arguments["namespace"]
71+
if ns == nil {
72+
ns = ""
73+
}
74+
name := ctr.Params.Arguments["name"]
75+
if name == nil {
76+
return NewTextResult("", errors.New("failed to get pod, missing argument name")), nil
77+
}
78+
ret, err := k.PodsGet(ctx, ns.(string), name.(string))
79+
if err != nil {
80+
return NewTextResult("", fmt.Errorf("failed to get pod %s in namespace %s: %v", name, ns, err)), nil
81+
}
82+
return NewTextResult(ret, err), nil
83+
}

0 commit comments

Comments
 (0)