mirror of
https://github.com/openshift/openshift-mcp-server.git
synced 2025-10-17 14:27:48 +03:00
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:
@@ -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`
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user