diff --git a/README.md b/README.md index d047fbb..c3b1632 100644 --- a/README.md +++ b/README.md @@ -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` diff --git a/pkg/kubernetes/pods.go b/pkg/kubernetes/pods.go index 1582964..ca5f2d5 100644 --- a/pkg/kubernetes/pods.go +++ b/pkg/kubernetes/pods.go @@ -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{ diff --git a/pkg/mcp/pods.go b/pkg/mcp/pods.go index 0d3f72a..c75e5dc 100644 --- a/pkg/mcp/pods.go +++ b/pkg/mcp/pods.go @@ -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 == "" { diff --git a/pkg/mcp/pods_exec_test.go b/pkg/mcp/pods_exec_test.go index 02ee4e9..39c5b10 100644 --- a/pkg/mcp/pods_exec_test.go +++ b/pkg/mcp/pods_exec_test.go @@ -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) } })