test: pods_exec tests executed from mcp client

This commit is contained in:
Marc Nuri
2025-03-30 19:25:31 +02:00
parent 8dc7160ff0
commit cbf0299e97
5 changed files with 73 additions and 80 deletions

View File

@@ -36,7 +36,6 @@ type Kubernetes struct {
CloseWatchKubeConfig CloseWatchKubeConfig
scheme *runtime.Scheme
parameterCodec runtime.ParameterCodec
restClient rest.Interface
clientSet kubernetes.Interface
discoveryClient *discovery.DiscoveryClient
deferredDiscoveryRESTMapper *restmapper.DeferredDiscoveryRESTMapper
@@ -48,11 +47,7 @@ func NewKubernetes() (*Kubernetes, error) {
if err != nil {
return nil, err
}
restClient, err := rest.HTTPClientFor(cfg)
if err != nil {
return nil, err
}
clientSet, err := kubernetes.NewForConfigAndClient(cfg, restClient)
clientSet, err := kubernetes.NewForConfig(cfg)
if err != nil {
return nil, err
}

View File

@@ -214,7 +214,7 @@ func (k *Kubernetes) PodsExec(ctx context.Context, namespace, name, container st
func (k *Kubernetes) createExecutor(namespace, name string, podExecOptions *v1.PodExecOptions) (remotecommand.Executor, error) {
// Compute URL
// https://github.com/kubernetes/kubectl/blob/5366de04e168bcbc11f5e340d131a9ca8b7d0df4/pkg/cmd/exec/exec.go#L382-L397
req := k.restClient.
req := k.clientSet.CoreV1().RESTClient().
Post().
Resource("pods").
Namespace(namespace).

View File

@@ -1,53 +0,0 @@
package kubernetes
import (
"bytes"
"context"
"io"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"net/http"
"testing"
)
func TestPodsExec(t *testing.T) {
mockServer := NewMockServer()
defer mockServer.Close()
mockServer.Handle(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.URL.Path != "/api/v1/namespaces/default/pods/pod-to-exec/exec" {
return
}
var stdin, stdout bytes.Buffer
ctx, err := createHTTPStreams(w, req, &StreamOptions{
Stdin: &stdin,
Stdout: &stdout,
})
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(err.Error()))
return
}
defer ctx.conn.Close()
_, _ = io.WriteString(ctx.stdoutStream, "total 0\n")
}))
mockServer.Handle(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.URL.Path != "/api/v1/namespaces/default/pods/pod-to-exec" {
return
}
writeObject(w, &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "pod-to-exec",
},
Spec: v1.PodSpec{Containers: []v1.Container{{Name: "container-to-exec"}}},
})
}))
k8s := mockServer.NewKubernetes()
out, err := k8s.PodsExec(context.Background(), "default", "pod-to-exec", "", []string{"ls", "-l"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if out != "total 0\n" {
t.Fatalf("unexpected output: %s", out)
}
}

View File

@@ -1,4 +1,4 @@
package kubernetes
package mcp
import (
"encoding/json"
@@ -10,26 +10,21 @@ import (
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/apimachinery/pkg/util/httpstream/spdy"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"net/http"
"net/http/httptest"
)
type MockServer struct {
server *httptest.Server
config *rest.Config
restClient *rest.RESTClient
restHandlers []http.HandlerFunc
clientSet kubernetes.Interface
parameterCodec runtime.ParameterCodec
server *httptest.Server
config *rest.Config
restHandlers []http.HandlerFunc
}
func NewMockServer() *MockServer {
ms := &MockServer{}
scheme := runtime.NewScheme()
codecs := serializer.NewCodecFactory(scheme)
ms.parameterCodec = runtime.NewParameterCodec(scheme)
ms.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
for _, handler := range ms.restHandlers {
handler(w, req)
@@ -44,9 +39,7 @@ func NewMockServer() *MockServer {
GroupVersion: &v1.SchemeGroupVersion,
},
}
ms.restClient, _ = rest.RESTClientFor(ms.config)
ms.restHandlers = make([]http.HandlerFunc, 0)
ms.clientSet = kubernetes.NewForConfigOrDie(ms.config)
return ms
}
@@ -58,15 +51,6 @@ func (m *MockServer) Handle(handler http.Handler) {
m.restHandlers = append(m.restHandlers, handler.ServeHTTP)
}
func (m *MockServer) NewKubernetes() *Kubernetes {
return &Kubernetes{
cfg: m.config,
restClient: m.restClient,
clientSet: m.clientSet,
parameterCodec: m.parameterCodec,
}
}
func writeObject(w http.ResponseWriter, obj runtime.Object) {
w.Header().Set("Content-Type", runtime.ContentTypeJSON)
if err := json.NewEncoder(w).Encode(obj); err != nil {

67
pkg/mcp/pods_exec_test.go Normal file
View File

@@ -0,0 +1,67 @@
package mcp
import (
"bytes"
"github.com/mark3labs/mcp-go/mcp"
"io"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"net/http"
"strings"
"testing"
)
func TestPodsExec(t *testing.T) {
testCase(t, func(c *mcpContext) {
mockServer := NewMockServer()
defer mockServer.Close()
c.withKubeConfig(mockServer.config)
mockServer.Handle(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.URL.Path != "/api/v1/namespaces/default/pods/pod-to-exec/exec" {
return
}
var stdin, stdout bytes.Buffer
ctx, err := createHTTPStreams(w, req, &StreamOptions{
Stdin: &stdin,
Stdout: &stdout,
})
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(err.Error()))
return
}
defer ctx.conn.Close()
_, _ = io.WriteString(ctx.stdoutStream, strings.Join(req.URL.Query()["command"], " "))
_, _ = io.WriteString(ctx.stdoutStream, "\ntotal 0\n")
}))
mockServer.Handle(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.URL.Path != "/api/v1/namespaces/default/pods/pod-to-exec" {
return
}
writeObject(w, &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "pod-to-exec",
},
Spec: v1.PodSpec{Containers: []v1.Container{{Name: "container-to-exec"}}},
})
}))
toolResult, err := c.callTool("pods_exec", map[string]interface{}{
"namespace": "default",
"name": "pod-to-exec",
"command": []interface{}{"ls", "-l"},
})
t.Run("pods_exec returns command output", func(t *testing.T) {
if err != nil {
t.Fatalf("call tool failed %v", err)
}
if toolResult.IsError {
t.Fatalf("call tool failed")
}
if toolResult.Content[0].(mcp.TextContent).Text != "ls -l\ntotal 0\n" {
t.Errorf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
}
})
})
}