Skip to content

Commit 6f1fabf

Browse files
committed
added: execute command in pod by @otischan
1 parent 5cad7db commit 6f1fabf

File tree

3 files changed

+94
-10
lines changed

3 files changed

+94
-10
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
case: Execute command in busybox pod
2+
3+
in:
4+
{
5+
"jsonrpc": "2.0",
6+
"method": "tools/call",
7+
"id": 2,
8+
"params":
9+
{
10+
"name": "k8s-pod-exec",
11+
"arguments":
12+
{
13+
"context": "k3d-mcp-k8s-integration-test",
14+
"namespace": "test",
15+
"pod": "busybox",
16+
"command": "echo HELLO FROM BUSYBOX",
17+
},
18+
},
19+
}
20+
out:
21+
{
22+
"jsonrpc": "2.0",
23+
"id": 2,
24+
"result":
25+
{ "content": [{ "type": "text", "text": '{"stdout":"HELLO FROM BUSYBOX\n","stderr":""}' }], "isError": false },
26+
}

internal/tools/pod_exec_cmd.go

+32-10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"fmt"
77
"strings"
8+
"time"
89

910
"github.com/strowk/mcp-k8s-go/internal/k8s"
1011
"github.com/strowk/mcp-k8s-go/internal/utils"
@@ -20,16 +21,20 @@ import (
2021
"k8s.io/client-go/tools/remotecommand"
2122
)
2223

24+
const timeout = 5 * time.Second
25+
2326
func NewPodExecCommandTool(pool k8s.ClientPool) fxctx.Tool {
2427
k8sNamespace := "namespace"
25-
k8sPodName := "podName"
28+
k8sPodName := "pod"
2629
execCommand := "command"
2730
k8sContext := "context"
31+
stdin := "stdin"
2832
schema := toolinput.NewToolInputSchema(
29-
toolinput.WithRequiredString(k8sNamespace, "The name of the namespace where the pod to execute the command is located."),
30-
toolinput.WithRequiredString(k8sPodName, "The name of the pod in which the command needs to be executed."),
31-
toolinput.WithRequiredString(execCommand, "The command to be executed inside the pod."),
32-
toolinput.WithString(k8sContext, "Kubernetes context name."),
33+
toolinput.WithString(k8sContext, "Kubernetes context name, defaults to current context"),
34+
toolinput.WithRequiredString(k8sNamespace, "Namespace where pod is located"),
35+
toolinput.WithRequiredString(k8sPodName, "Name of the pod to execute command in"),
36+
toolinput.WithRequiredString(execCommand, "Command to be executed"),
37+
toolinput.WithString(stdin, "Standard input to the command, defaults to empty string"),
3338
)
3439
return fxctx.NewTool(
3540
&mcp.Tool{
@@ -54,14 +59,15 @@ func NewPodExecCommandTool(pool k8s.ClientPool) fxctx.Tool {
5459
if err != nil {
5560
return errResponse(fmt.Errorf("invalid input command: %w", err))
5661
}
57-
k8sContext := input.StringOr(k8sContext, "default")
62+
k8sContext := input.StringOr(k8sContext, "")
63+
stdin := input.StringOr(stdin, "")
5864

5965
kubeconfig := k8s.GetKubeConfigForContext(k8sContext)
6066
config, err := kubeconfig.ClientConfig()
6167
if err != nil {
6268
return errResponse(fmt.Errorf("invalid config: %w", err))
6369
}
64-
execResult, err := cmdExecuter(pool, config, k8sPodName, k8sNamespace, execCommand, k8sContext)
70+
execResult, err := cmdExecuter(pool, config, k8sPodName, k8sNamespace, execCommand, k8sContext, stdin)
6571
if err != nil {
6672
return errResponse(fmt.Errorf("command execute failed: %w", err))
6773
}
@@ -88,7 +94,15 @@ type ExecResult struct {
8894
Stderr interface{} `json:"stderr"`
8995
}
9096

91-
func cmdExecuter(pool k8s.ClientPool, config *rest.Config, podName, namespace, cmd, k8sContext string) (ExecResult, error) {
97+
func cmdExecuter(
98+
pool k8s.ClientPool,
99+
config *rest.Config,
100+
podName,
101+
namespace,
102+
cmd,
103+
k8sContext,
104+
stdin string,
105+
) (ExecResult, error) {
92106
execResult := ExecResult{}
93107
clientset, err := pool.GetClientset(k8sContext)
94108
if err != nil {
@@ -122,12 +136,20 @@ func cmdExecuter(pool k8s.ClientPool, config *rest.Config, podName, namespace, c
122136
if err != nil {
123137
return execResult, err
124138
}
139+
140+
ctx := context.Background()
141+
withTimeout, cancel := context.WithTimeout(ctx, timeout)
142+
defer cancel() // release resources if operation finishes before timeout
143+
125144
var stdout, stderr bytes.Buffer
126-
if err = executor.Stream(remotecommand.StreamOptions{
127-
Stdin: strings.NewReader(""),
145+
if err = executor.StreamWithContext(withTimeout, remotecommand.StreamOptions{
146+
Stdin: strings.NewReader(stdin),
128147
Stdout: &stdout,
129148
Stderr: &stderr,
130149
}); err != nil {
150+
if err == context.DeadlineExceeded {
151+
return execResult, fmt.Errorf("command timed out after %s", timeout)
152+
}
131153
return execResult, err
132154
}
133155
execResult.Stdout = stdout.String()

testdata/list_tools_test.yaml

+36
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,42 @@ out:
9191
"required": ["namespace", "kind", "name"],
9292
},
9393
},
94+
{
95+
"name": "k8s-pod-exec",
96+
"description": "Execute command in Kubernetes pod",
97+
"inputSchema":
98+
{
99+
"type": "object",
100+
"properties":
101+
{
102+
"context":
103+
{
104+
"type": "string",
105+
"description": "Kubernetes context name, defaults to current context",
106+
},
107+
"namespace":
108+
{
109+
"type": "string",
110+
"description": "Namespace where pod is located",
111+
},
112+
"pod":
113+
{
114+
"type": "string",
115+
"description": "Name of the pod to execute command in",
116+
},
117+
"command":
118+
{
119+
"type": "string",
120+
"description": "Command to be executed",
121+
},
122+
"stdin":
123+
{
124+
"type": "string",
125+
"description": "Standard input to the command, defaults to empty string",
126+
},
127+
},
128+
},
129+
},
94130
{
95131
"name": "list-k8s-contexts",
96132
"description": "List Kubernetes contexts from configuration files such as kubeconfig",

0 commit comments

Comments
 (0)