Files
openshift-mcp-server/pkg/mcp/pods_exec_test.go
Marc Nuri 48cf204a89 feat(toolsets): add support for multiple toolsets in configuration (#323)
Users can now enable or disable different toolsets either by providing
a command-line flag or by setting the toolsets array field in the TOML
configuration.

Downstream Kubernetes API developers can declare toolsets for their
APIs by creating a new nested package in pkg/toolsets and registering
it in pkg/mcp/modules.go

Signed-off-by: Marc Nuri <marc@marcnuri.com>
2025-09-17 10:53:56 +02:00

132 lines
4.7 KiB
Go

package mcp
import (
"bytes"
"io"
"net/http"
"strings"
"testing"
"github.com/mark3labs/mcp-go/mcp"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/containers/kubernetes-mcp-server/internal/test"
"github.com/containers/kubernetes-mcp-server/pkg/config"
)
func TestPodsExec(t *testing.T) {
testCase(t, func(c *mcpContext) {
mockServer := test.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 := test.CreateHTTPStreams(w, req, &test.StreamOptions{
Stdin: &stdin,
Stdout: &stdout,
})
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(err.Error()))
return
}
defer func(conn io.Closer) { _ = conn.Close() }(ctx.Closer)
_, _ = io.WriteString(ctx.StdoutStream, "command:"+strings.Join(req.URL.Query()["command"], " ")+"\n")
_, _ = io.WriteString(ctx.StdoutStream, "container:"+strings.Join(req.URL.Query()["container"], " ")+"\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
}
test.WriteObject(w, &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "pod-to-exec",
},
Spec: v1.PodSpec{Containers: []v1.Container{{Name: "container-to-exec"}}},
})
}))
podsExecNilNamespace, err := c.callTool("pods_exec", map[string]interface{}{
"name": "pod-to-exec",
"command": []interface{}{"ls", "-l"},
})
t.Run("pods_exec with name and nil namespace returns command output", func(t *testing.T) {
if err != nil {
t.Fatalf("call tool failed %v", err)
}
if podsExecNilNamespace.IsError {
t.Fatalf("call tool failed: %v", podsExecNilNamespace.Content)
}
if !strings.Contains(podsExecNilNamespace.Content[0].(mcp.TextContent).Text, "command:ls -l\n") {
t.Errorf("unexpected result %v", podsExecNilNamespace.Content[0].(mcp.TextContent).Text)
}
})
podsExecInNamespace, err := c.callTool("pods_exec", map[string]interface{}{
"namespace": "default",
"name": "pod-to-exec",
"command": []interface{}{"ls", "-l"},
})
t.Run("pods_exec with name and namespace returns command output", func(t *testing.T) {
if err != nil {
t.Fatalf("call tool failed %v", err)
}
if podsExecInNamespace.IsError {
t.Fatalf("call tool failed: %v", podsExecInNamespace.Content)
}
if !strings.Contains(podsExecInNamespace.Content[0].(mcp.TextContent).Text, "command:ls -l\n") {
t.Errorf("unexpected result %v", podsExecInNamespace.Content[0].(mcp.TextContent).Text)
}
})
podsExecInNamespaceAndContainer, err := c.callTool("pods_exec", map[string]interface{}{
"namespace": "default",
"name": "pod-to-exec",
"command": []interface{}{"ls", "-l"},
"container": "a-specific-container",
})
t.Run("pods_exec with name, namespace, and container returns command output", func(t *testing.T) {
if err != nil {
t.Fatalf("call tool failed %v", err)
}
if podsExecInNamespaceAndContainer.IsError {
t.Fatalf("call tool failed")
}
if !strings.Contains(podsExecInNamespaceAndContainer.Content[0].(mcp.TextContent).Text, "command:ls -l\n") {
t.Errorf("unexpected result %v", podsExecInNamespaceAndContainer.Content[0].(mcp.TextContent).Text)
}
if !strings.Contains(podsExecInNamespaceAndContainer.Content[0].(mcp.TextContent).Text, "container:a-specific-container\n") {
t.Errorf("expected container name not found %v", podsExecInNamespaceAndContainer.Content[0].(mcp.TextContent).Text)
}
})
})
}
func TestPodsExecDenied(t *testing.T) {
deniedResourcesServer := test.Must(config.ReadToml([]byte(`
denied_resources = [ { version = "v1", kind = "Pod" } ]
`)))
testCaseWithContext(t, &mcpContext{staticConfig: deniedResourcesServer}, func(c *mcpContext) {
c.withEnvTest()
podsRun, _ := c.callTool("pods_exec", map[string]interface{}{
"namespace": "default",
"name": "pod-to-exec",
"command": []interface{}{"ls", "-l"},
"container": "a-specific-container",
})
t.Run("pods_exec has error", func(t *testing.T) {
if !podsRun.IsError {
t.Fatalf("call tool should fail")
}
})
t.Run("pods_exec describes denial", func(t *testing.T) {
expectedMessage := "failed to exec in pod pod-to-exec in namespace default: resource not allowed: /v1, Kind=Pod"
if podsRun.Content[0].(mcp.TextContent).Text != expectedMessage {
t.Fatalf("expected descriptive error '%s', got %v", expectedMessage, podsRun.Content[0].(mcp.TextContent).Text)
}
})
})
}