Skip to content

Commit 37d7175

Browse files
authored
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).
1 parent 22a7125 commit 37d7175

File tree

4 files changed

+60
-17
lines changed

4 files changed

+60
-17
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@ Execute a command in a Kubernetes Pod in the current or provided namespace with
185185
- Name of the Pod
186186
- `namespace` (string, required)
187187
- Namespace of the Pod
188+
- `container` (`string`, optional)
189+
- Name of the Pod container to get logs from
188190

189191
### `pods_get`
190192

pkg/kubernetes/pods.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -184,18 +184,19 @@ func (k *Kubernetes) PodsExec(ctx context.Context, namespace, name, container st
184184
if pod.Status.Phase == v1.PodSucceeded || pod.Status.Phase == v1.PodFailed {
185185
return "", fmt.Errorf("cannot exec into a container in a completed pod; current phase is %s", pod.Status.Phase)
186186
}
187+
if container == "" {
188+
container = pod.Spec.Containers[0].Name
189+
}
187190
podExecOptions := &v1.PodExecOptions{
188-
Command: command,
189-
Stdout: true,
190-
Stderr: true,
191+
Container: container,
192+
Command: command,
193+
Stdout: true,
194+
Stderr: true,
191195
}
192196
executor, err := k.createExecutor(namespace, name, podExecOptions)
193197
if err != nil {
194198
return "", err
195199
}
196-
if container == "" {
197-
container = pod.Spec.Containers[0].Name
198-
}
199200
stdout := bytes.NewBuffer(make([]byte, 0))
200201
stderr := bytes.NewBuffer(make([]byte, 0))
201202
if err = executor.StreamWithContext(ctx, remotecommand.StreamOptions{

pkg/mcp/pods.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ func (s *Server) initPods() []server.ServerTool {
2929
), s.podsDelete},
3030
{mcp.NewTool("pods_exec",
3131
mcp.WithDescription("Execute a command in a Kubernetes Pod in the current or provided namespace with the provided name and command"),
32-
mcp.WithString("namespace", mcp.Description("Namespace to get the Pod logs from")),
33-
mcp.WithString("name", mcp.Description("Name of the Pod to get the logs from"), mcp.Required()),
32+
mcp.WithString("namespace", mcp.Description("Namespace of the Pod where the command will be executed")),
33+
mcp.WithString("name", mcp.Description("Name of the Pod where the command will be executed"), mcp.Required()),
3434
mcp.WithArray("command", mcp.Description("Command to execute in the Pod container. "+
3535
"The first item is the command to be run, and the rest are the arguments to that command. "+
3636
`Example: ["ls", "-l", "/tmp"]`),
@@ -44,6 +44,7 @@ func (s *Server) initPods() []server.ServerTool {
4444
},
4545
mcp.Required(),
4646
),
47+
mcp.WithString("container", mcp.Description("Name of the Pod container where the command will be executed (Optional)")),
4748
), s.podsExec},
4849
{mcp.NewTool("pods_log",
4950
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
122123
if name == nil {
123124
return NewTextResult("", errors.New("failed to exec in pod, missing argument name")), nil
124125
}
126+
container := ctr.Params.Arguments["container"]
127+
if container == nil {
128+
container = ""
129+
}
125130
commandArg := ctr.Params.Arguments["command"]
126131
command := make([]string, 0)
127132
if _, ok := commandArg.([]interface{}); ok {
@@ -133,7 +138,7 @@ func (s *Server) podsExec(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.Ca
133138
} else {
134139
return NewTextResult("", errors.New("failed to exec in pod, invalid command argument")), nil
135140
}
136-
ret, err := s.k.PodsExec(ctx, ns.(string), name.(string), "", command)
141+
ret, err := s.k.PodsExec(ctx, ns.(string), name.(string), container.(string), command)
137142
if err != nil {
138143
return NewTextResult("", fmt.Errorf("failed to exec in pod %s in namespace %s: %v", name, ns, err)), nil
139144
} else if ret == "" {

pkg/mcp/pods_exec_test.go

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ func TestPodsExec(t *testing.T) {
3030
_, _ = w.Write([]byte(err.Error()))
3131
return
3232
}
33-
defer ctx.conn.Close()
34-
_, _ = io.WriteString(ctx.stdoutStream, strings.Join(req.URL.Query()["command"], " "))
35-
_, _ = io.WriteString(ctx.stdoutStream, "\ntotal 0\n")
33+
defer func(conn io.Closer) { _ = conn.Close() }(ctx.conn)
34+
_, _ = io.WriteString(ctx.stdoutStream, "command:"+strings.Join(req.URL.Query()["command"], " ")+"\n")
35+
_, _ = io.WriteString(ctx.stdoutStream, "container:"+strings.Join(req.URL.Query()["container"], " ")+"\n")
3636
}))
3737
mockServer.Handle(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
3838
if req.URL.Path != "/api/v1/namespaces/default/pods/pod-to-exec" {
@@ -46,20 +46,55 @@ func TestPodsExec(t *testing.T) {
4646
Spec: v1.PodSpec{Containers: []v1.Container{{Name: "container-to-exec"}}},
4747
})
4848
}))
49-
toolResult, err := c.callTool("pods_exec", map[string]interface{}{
49+
podsExecNilNamespace, err := c.callTool("pods_exec", map[string]interface{}{
50+
"name": "pod-to-exec",
51+
"command": []interface{}{"ls", "-l"},
52+
})
53+
t.Run("pods_exec with name and nil namespace returns command output", func(t *testing.T) {
54+
if err != nil {
55+
t.Fatalf("call tool failed %v", err)
56+
}
57+
if podsExecNilNamespace.IsError {
58+
t.Fatalf("call tool failed")
59+
}
60+
if !strings.Contains(podsExecNilNamespace.Content[0].(mcp.TextContent).Text, "command:ls -l\n") {
61+
t.Errorf("unexpected result %v", podsExecNilNamespace.Content[0].(mcp.TextContent).Text)
62+
}
63+
})
64+
podsExecInNamespace, err := c.callTool("pods_exec", map[string]interface{}{
65+
"namespace": "default",
66+
"name": "pod-to-exec",
67+
"command": []interface{}{"ls", "-l"},
68+
})
69+
t.Run("pods_exec with name and namespace returns command output", func(t *testing.T) {
70+
if err != nil {
71+
t.Fatalf("call tool failed %v", err)
72+
}
73+
if podsExecInNamespace.IsError {
74+
t.Fatalf("call tool failed")
75+
}
76+
if !strings.Contains(podsExecNilNamespace.Content[0].(mcp.TextContent).Text, "command:ls -l\n") {
77+
t.Errorf("unexpected result %v", podsExecInNamespace.Content[0].(mcp.TextContent).Text)
78+
}
79+
})
80+
podsExecInNamespaceAndContainer, err := c.callTool("pods_exec", map[string]interface{}{
5081
"namespace": "default",
5182
"name": "pod-to-exec",
5283
"command": []interface{}{"ls", "-l"},
84+
"container": "a-specific-container",
5385
})
54-
t.Run("pods_exec returns command output", func(t *testing.T) {
86+
t.Run("pods_exec with name, namespace, and container returns command output", func(t *testing.T) {
5587
if err != nil {
5688
t.Fatalf("call tool failed %v", err)
5789
}
58-
if toolResult.IsError {
90+
if podsExecInNamespaceAndContainer.IsError {
5991
t.Fatalf("call tool failed")
6092
}
61-
if toolResult.Content[0].(mcp.TextContent).Text != "ls -l\ntotal 0\n" {
62-
t.Errorf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
93+
if !strings.Contains(podsExecInNamespaceAndContainer.Content[0].(mcp.TextContent).Text, "command:ls -l\n") {
94+
t.Errorf("unexpected result %v", podsExecInNamespaceAndContainer.Content[0].(mcp.TextContent).Text)
95+
}
96+
if !strings.Contains(podsExecInNamespaceAndContainer.Content[0].(mcp.TextContent).Text, "container:a-specific-container\n") {
97+
t.Errorf("expected container name not found %v", podsExecInNamespaceAndContainer.Content[0].(mcp.TextContent).Text)
6398
}
6499
})
65100

0 commit comments

Comments
 (0)