Skip to content

Commit 5cad7db

Browse files
authored
Merge pull request #25 from otischan/main
execute command in pod
2 parents 2802bcb + 3556b53 commit 5cad7db

File tree

4 files changed

+148
-0
lines changed

4 files changed

+148
-0
lines changed

go.mod

+3
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,15 @@ require (
3030
github.com/google/go-cmp v0.6.0 // indirect
3131
github.com/google/gofuzz v1.2.0 // indirect
3232
github.com/google/uuid v1.6.0 // indirect
33+
github.com/gorilla/websocket v1.5.0 // indirect
3334
github.com/josharian/intern v1.0.0 // indirect
3435
github.com/json-iterator/go v1.1.12 // indirect
3536
github.com/mailru/easyjson v0.7.7 // indirect
37+
github.com/moby/spdystream v0.5.0 // indirect
3638
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
3739
github.com/modern-go/reflect2 v1.0.2 // indirect
3840
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
41+
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
3942
github.com/pkg/errors v0.9.1 // indirect
4043
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
4144
github.com/spf13/pflag v1.0.5 // indirect

go.sum

+8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
2+
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
13
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
24
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
35
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -35,6 +37,8 @@ github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgY
3537
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
3638
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
3739
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
40+
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
41+
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
3842
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
3943
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
4044
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -50,13 +54,17 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
5054
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
5155
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
5256
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
57+
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
58+
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
5359
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
5460
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
5561
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
5662
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
5763
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
5864
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
5965
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
66+
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
67+
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
6068
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
6169
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
6270
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=

internal/tools/pod_exec_cmd.go

+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package tools
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"strings"
8+
9+
"github.com/strowk/mcp-k8s-go/internal/k8s"
10+
"github.com/strowk/mcp-k8s-go/internal/utils"
11+
12+
"github.com/strowk/foxy-contexts/pkg/fxctx"
13+
"github.com/strowk/foxy-contexts/pkg/mcp"
14+
"github.com/strowk/foxy-contexts/pkg/toolinput"
15+
16+
corev1 "k8s.io/api/core/v1"
17+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
18+
"k8s.io/client-go/kubernetes/scheme"
19+
"k8s.io/client-go/rest"
20+
"k8s.io/client-go/tools/remotecommand"
21+
)
22+
23+
func NewPodExecCommandTool(pool k8s.ClientPool) fxctx.Tool {
24+
k8sNamespace := "namespace"
25+
k8sPodName := "podName"
26+
execCommand := "command"
27+
k8sContext := "context"
28+
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+
)
34+
return fxctx.NewTool(
35+
&mcp.Tool{
36+
Name: "k8s-pod-exec",
37+
Description: utils.Ptr("Execute command in Kubernetes pod"),
38+
InputSchema: schema.GetMcpToolInputSchema(),
39+
},
40+
func(args map[string]interface{}) *mcp.CallToolResult {
41+
input, err := schema.Validate(args)
42+
if err != nil {
43+
return errResponse(err)
44+
}
45+
k8sNamespace, err := input.String(k8sNamespace)
46+
if err != nil {
47+
return errResponse(fmt.Errorf("invalid input namespace: %w", err))
48+
}
49+
k8sPodName, err := input.String(k8sPodName)
50+
if err != nil {
51+
return errResponse(fmt.Errorf("invalid input pod: %w", err))
52+
}
53+
execCommand, err := input.String(execCommand)
54+
if err != nil {
55+
return errResponse(fmt.Errorf("invalid input command: %w", err))
56+
}
57+
k8sContext := input.StringOr(k8sContext, "default")
58+
59+
kubeconfig := k8s.GetKubeConfigForContext(k8sContext)
60+
config, err := kubeconfig.ClientConfig()
61+
if err != nil {
62+
return errResponse(fmt.Errorf("invalid config: %w", err))
63+
}
64+
execResult, err := cmdExecuter(pool, config, k8sPodName, k8sNamespace, execCommand, k8sContext)
65+
if err != nil {
66+
return errResponse(fmt.Errorf("command execute failed: %w", err))
67+
}
68+
69+
var content mcp.TextContent
70+
contents := []interface{}{}
71+
content, err = NewJsonContent(execResult)
72+
if err != nil {
73+
return errResponse(err)
74+
}
75+
contents = append(contents, content)
76+
77+
return &mcp.CallToolResult{
78+
Meta: map[string]interface{}{},
79+
Content: contents,
80+
IsError: utils.Ptr(false),
81+
}
82+
},
83+
)
84+
}
85+
86+
type ExecResult struct {
87+
Stdout interface{} `json:"stdout"`
88+
Stderr interface{} `json:"stderr"`
89+
}
90+
91+
func cmdExecuter(pool k8s.ClientPool, config *rest.Config, podName, namespace, cmd, k8sContext string) (ExecResult, error) {
92+
execResult := ExecResult{}
93+
clientset, err := pool.GetClientset(k8sContext)
94+
if err != nil {
95+
return execResult, err
96+
}
97+
98+
pod, err := clientset.CoreV1().Pods(namespace).Get(context.TODO(), podName, metav1.GetOptions{})
99+
if err != nil {
100+
return execResult, err
101+
}
102+
103+
if len(pod.Spec.Containers) == 0 {
104+
return execResult, fmt.Errorf("Pod %s has no containers", podName)
105+
}
106+
107+
containerName := pod.Spec.Containers[0].Name
108+
req := clientset.CoreV1().RESTClient().Post().
109+
Resource("pods").
110+
Name(podName).
111+
Namespace(namespace).
112+
SubResource("exec").
113+
VersionedParams(&corev1.PodExecOptions{
114+
Container: containerName,
115+
Command: []string{"sh", "-c", cmd},
116+
Stdin: true,
117+
Stdout: true,
118+
Stderr: true,
119+
TTY: false,
120+
}, scheme.ParameterCodec)
121+
executor, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
122+
if err != nil {
123+
return execResult, err
124+
}
125+
var stdout, stderr bytes.Buffer
126+
if err = executor.Stream(remotecommand.StreamOptions{
127+
Stdin: strings.NewReader(""),
128+
Stdout: &stdout,
129+
Stderr: &stderr,
130+
}); err != nil {
131+
return execResult, err
132+
}
133+
execResult.Stdout = stdout.String()
134+
execResult.Stderr = stderr.String()
135+
return execResult, nil
136+
}

main.go

+1
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ func getApp() *app.Builder {
111111
WithTool(tools.NewGetResourceTool).
112112
WithTool(tools.NewListNodesTool).
113113
WithTool(tools.NewListEventsTool).
114+
WithTool(tools.NewPodExecCommandTool).
114115
WithPrompt(prompts.NewListPodsPrompt).
115116
WithPrompt(prompts.NewListNamespacesPrompt).
116117
WithResourceProvider(resources.NewContextsResourceProvider).

0 commit comments

Comments
 (0)