feat(pods): pods_exec supports specifying container

Allows to specify the container where the command will be executed.
Additionally, prevents the command from failing in pods with multiple
containers when the container is not specified (defaults to first).
This commit is contained in:
Marc Nuri
2025-04-26 07:02:26 +02:00
committed by GitHub
parent 22a7125f4a
commit 37d7175cd6
4 changed files with 60 additions and 17 deletions

View File

@@ -185,6 +185,8 @@ Execute a command in a Kubernetes Pod in the current or provided namespace with
- Name of the Pod
- `namespace` (string, required)
- Namespace of the Pod
- `container` (`string`, optional)
- Name of the Pod container to get logs from
### `pods_get`

View File

@@ -184,18 +184,19 @@ func (k *Kubernetes) PodsExec(ctx context.Context, namespace, name, container st
if pod.Status.Phase == v1.PodSucceeded || pod.Status.Phase == v1.PodFailed {
return "", fmt.Errorf("cannot exec into a container in a completed pod; current phase is %s", pod.Status.Phase)
}
if container == "" {
container = pod.Spec.Containers[0].Name
}
podExecOptions := &v1.PodExecOptions{
Command: command,
Stdout: true,
Stderr: true,
Container: container,
Command: command,
Stdout: true,
Stderr: true,
}
executor, err := k.createExecutor(namespace, name, podExecOptions)
if err != nil {
return "", err
}
if container == "" {
container = pod.Spec.Containers[0].Name
}
stdout := bytes.NewBuffer(make([]byte, 0))
stderr := bytes.NewBuffer(make([]byte, 0))
if err = executor.StreamWithContext(ctx, remotecommand.StreamOptions{

View File

@@ -29,8 +29,8 @@ func (s *Server) initPods() []server.ServerTool {
), s.podsDelete},
{mcp.NewTool("pods_exec",
mcp.WithDescription("Execute a command in a Kubernetes Pod in the current or provided namespace with the provided name and command"),
mcp.WithString("namespace", mcp.Description("Namespace to get the Pod logs from")),
mcp.WithString("name", mcp.Description("Name of the Pod to get the logs from"), mcp.Required()),
mcp.WithString("namespace", mcp.Description("Namespace of the Pod where the command will be executed")),
mcp.WithString("name", mcp.Description("Name of the Pod where the command will be executed"), mcp.Required()),
mcp.WithArray("command", mcp.Description("Command to execute in the Pod container. "+
"The first item is the command to be run, and the rest are the arguments to that command. "+
`Example: ["ls", "-l", "/tmp"]`),
@@ -44,6 +44,7 @@ func (s *Server) initPods() []server.ServerTool {
},
mcp.Required(),
),
mcp.WithString("container", mcp.Description("Name of the Pod container where the command will be executed (Optional)")),
), s.podsExec},
{mcp.NewTool("pods_log",
mcp.WithDescription("Get the logs of a Kubernetes Pod in the current or provided namespace with the provided name"),
@@ -122,6 +123,10 @@ func (s *Server) podsExec(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.Ca
if name == nil {
return NewTextResult("", errors.New("failed to exec in pod, missing argument name")), nil
}
container := ctr.Params.Arguments["container"]
if container == nil {
container = ""
}
commandArg := ctr.Params.Arguments["command"]
command := make([]string, 0)
if _, ok := commandArg.([]interface{}); ok {
@@ -133,7 +138,7 @@ func (s *Server) podsExec(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.Ca
} else {
return NewTextResult("", errors.New("failed to exec in pod, invalid command argument")), nil
}
ret, err := s.k.PodsExec(ctx, ns.(string), name.(string), "", command)
ret, err := s.k.PodsExec(ctx, ns.(string), name.(string), container.(string), command)
if err != nil {
return NewTextResult("", fmt.Errorf("failed to exec in pod %s in namespace %s: %v", name, ns, err)), nil
} else if ret == "" {

View File

@@ -30,9 +30,9 @@ func TestPodsExec(t *testing.T) {
_, _ = 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")
defer func(conn io.Closer) { _ = conn.Close() }(ctx.conn)
_, _ = 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" {
@@ -46,20 +46,55 @@ func TestPodsExec(t *testing.T) {
Spec: v1.PodSpec{Containers: []v1.Container{{Name: "container-to-exec"}}},
})
}))
toolResult, err := c.callTool("pods_exec", map[string]interface{}{
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")
}
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 returns command output", func(t *testing.T) {
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 toolResult.IsError {
if podsExecInNamespace.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)
if !strings.Contains(podsExecNilNamespace.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)
}
})