Skip to content

Commit fb155d7

Browse files
authored
Merge pull request #29 from StacklokLabs/feature/pod-exec
Add pod exec functionality via post_resource MCP tool
2 parents a85cf8e + 2a2b9b3 commit fb155d7

File tree

10 files changed

+1201
-4
lines changed

10 files changed

+1201
-4
lines changed

README.md

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ MKP is a Model Context Protocol (MCP) server for Kubernetes that allows LLM-powe
1414
- Get resources and their subresources (including status, scale, logs, etc.)
1515
- Apply (create or update) clustered resources
1616
- Apply (create or update) namespaced resources
17+
- Execute commands in pods with timeout control
1718
- Generic and pluggable implementation using API Machinery's unstructured client
1819
- Built-in rate limiting for protection against excessive API calls
1920

@@ -256,6 +257,74 @@ Example:
256257
}
257258
```
258259

260+
#### post_resource
261+
262+
Posts to a Kubernetes resource or its subresource, particularly useful for executing commands in pods.
263+
264+
Parameters:
265+
- `resource_type` (required): Type of resource to post to (clustered or namespaced)
266+
- `group`: API group (e.g., apps, networking.k8s.io)
267+
- `version` (required): API version (e.g., v1, v1beta1)
268+
- `resource` (required): Resource name (e.g., deployments, services)
269+
- `namespace`: Namespace (required for namespaced resources)
270+
- `name` (required): Name of the resource to post to
271+
- `subresource`: Subresource to post to (e.g., exec)
272+
- `body` (required): Body to post to the resource
273+
- `parameters`: Optional parameters for the request
274+
275+
Example of executing a command in a pod:
276+
277+
```json
278+
{
279+
"name": "post_resource",
280+
"arguments": {
281+
"resource_type": "namespaced",
282+
"group": "",
283+
"version": "v1",
284+
"resource": "pods",
285+
"namespace": "default",
286+
"name": "my-pod",
287+
"subresource": "exec",
288+
"body": {
289+
"command": ["ls", "-la", "/"],
290+
"container": "my-container",
291+
"timeout": 30
292+
}
293+
}
294+
}
295+
```
296+
297+
The `body` for pod exec supports the following fields:
298+
- `command` (required): Command to execute, either as a string or an array of strings
299+
- `container` (optional): Container name to execute the command in (defaults to the first container)
300+
- `timeout` (optional): Timeout in seconds (defaults to 15 seconds, maximum 60 seconds)
301+
302+
Note on timeouts:
303+
- Default timeout: 15 seconds if not specified
304+
- Maximum timeout: 60 seconds (any larger value will be capped)
305+
- Commands that exceed the timeout will be terminated and return a timeout error
306+
307+
The response includes stdout, stderr, and any error message:
308+
309+
```json
310+
{
311+
"apiVersion": "v1",
312+
"kind": "Pod",
313+
"metadata": {
314+
"name": "my-pod",
315+
"namespace": "default"
316+
},
317+
"spec": {
318+
"command": ["ls", "-la", "/"]
319+
},
320+
"status": {
321+
"stdout": "total 48\ndrwxr-xr-x 1 root root 4096 May 5 14:30 .\ndrwxr-xr-x 1 root root 4096 May 5 14:30 ..\n...",
322+
"stderr": "",
323+
"error": ""
324+
}
325+
}
326+
```
327+
259328
### MCP Resources
260329

261330
The MKP server provides access to Kubernetes resources through MCP resources. The resource URIs follow these formats:
@@ -279,11 +348,11 @@ You can disable this behavior by using the `--serve-resources` flag:
279348
./build/mkp-server --kubeconfig=/path/to/kubeconfig --serve-resources=false
280349
```
281350

282-
Even with resource discovery disabled, the MCP tools (`get_resource`, `list_resources`, and `apply_resource`) remain fully functional, allowing you to interact with your Kubernetes cluster.
351+
Even with resource discovery disabled, the MCP tools (`get_resource`, `list_resources`, `apply_resource`, `delete_resource`, and `post_resource`) remain fully functional, allowing you to interact with your Kubernetes cluster.
283352

284353
#### Enabling Write Operations
285354

286-
By default, MKP operates in read-only mode, meaning it does not allow write operations on the cluster, i.e. the `apply_resource` tool will not be available. You can enable write operations by using the `--read-write` flag:
355+
By default, MKP operates in read-only mode, meaning it does not allow write operations on the cluster, i.e. the `apply_resource`, `delete_resource`, and `post_resource` tools will not be available. You can enable write operations by using the `--read-write` flag:
287356

288357
```bash
289358
# Run with write operations enabled

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,15 @@ require (
2222
github.com/google/gnostic-models v0.6.9 // indirect
2323
github.com/google/go-cmp v0.7.0 // indirect
2424
github.com/google/uuid v1.6.0 // indirect
25+
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
2526
github.com/josharian/intern v1.0.0 // indirect
2627
github.com/json-iterator/go v1.1.12 // indirect
2728
github.com/mailru/easyjson v0.7.7 // indirect
29+
github.com/moby/spdystream v0.5.0 // indirect
2830
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
2931
github.com/modern-go/reflect2 v1.0.2 // indirect
3032
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
33+
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
3134
github.com/pkg/errors v0.9.1 // indirect
3235
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
3336
github.com/spf13/cast v1.7.1 // indirect

go.sum

Lines changed: 8 additions & 0 deletions
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=
@@ -33,6 +35,8 @@ github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgY
3335
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
3436
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
3537
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
38+
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
39+
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
3640
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
3741
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
3842
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -50,13 +54,17 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
5054
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
5155
github.com/mark3labs/mcp-go v0.27.0 h1:iok9kU4DUIU2/XVLgFS2Q9biIDqstC0jY4EQTK2Erzc=
5256
github.com/mark3labs/mcp-go v0.27.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
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=

pkg/k8s/client.go

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,21 @@ type PodLogsFunc func(
2626
parameters map[string]string,
2727
) (*unstructured.Unstructured, error)
2828

29+
// ExecInPodFunc is a function type for executing commands in pods
30+
type ExecInPodFunc func(
31+
ctx context.Context,
32+
namespace, name string,
33+
command []string,
34+
container string,
35+
timeout time.Duration,
36+
) (*unstructured.Unstructured, error)
37+
2938
// Client represents a Kubernetes client with discovery and dynamic capabilities
3039
type Client struct {
3140
discoveryClient discovery.DiscoveryInterface
3241
dynamicClient dynamic.Interface
3342
clientset kubernetes.Interface
34-
getPodLogs PodLogsFunc
43+
restConfig *rest.Config
3544
kubeconfigPath string
3645
mu sync.RWMutex // Protects access to client components
3746

@@ -41,6 +50,10 @@ type Client struct {
4150
refreshInterval time.Duration
4251
refreshing bool
4352
refreshMu sync.Mutex // Protects refreshing state
53+
54+
// function overrides for testing purposes
55+
getPodLogs PodLogsFunc
56+
execInPod ExecInPodFunc
4457
}
4558

4659
// NewClient creates a new Kubernetes client
@@ -72,11 +85,13 @@ func NewClient(kubeconfigPath string) (*Client, error) {
7285
discoveryClient: discoveryClient,
7386
dynamicClient: dynamicClient,
7487
clientset: clientset,
88+
restConfig: config,
7589
kubeconfigPath: kubeconfigPath,
7690
}
7791

78-
// Set the default implementation for getPodLogs
92+
// Set the default implementations
7993
client.getPodLogs = client.defaultGetPodLogs
94+
client.execInPod = client.defaultExecInPod
8095

8196
return client, nil
8297
}
@@ -273,6 +288,39 @@ func (c *Client) GetPodLogs() PodLogsFunc {
273288
return c.getPodLogs
274289
}
275290

291+
// SetExecInPodFunc sets the function used to execute commands in pods (for testing purposes)
292+
func (c *Client) SetExecInPodFunc(execInPodFunc ExecInPodFunc) {
293+
c.mu.Lock()
294+
defer c.mu.Unlock()
295+
296+
c.execInPod = execInPodFunc
297+
}
298+
299+
// GetExecInPodFunc returns the current exec in pod function
300+
func (c *Client) GetExecInPodFunc() ExecInPodFunc {
301+
c.mu.RLock()
302+
defer c.mu.RUnlock()
303+
304+
return c.execInPod
305+
}
306+
307+
// ExecInPod executes a command in a pod and returns the result
308+
// The command will be killed if it exceeds the timeout
309+
// If timeout is 0 or negative, the default timeout (15s) will be used
310+
// If timeout exceeds MaxExecTimeout, it will be capped at MaxExecTimeout
311+
func (c *Client) ExecInPod(
312+
ctx context.Context,
313+
namespace, name string,
314+
command []string,
315+
container string,
316+
timeout time.Duration,
317+
) (*unstructured.Unstructured, error) {
318+
c.mu.RLock()
319+
defer c.mu.RUnlock()
320+
321+
return c.execInPod(ctx, namespace, name, command, container, timeout)
322+
}
323+
276324
// IsReady returns true if the client is ready to use
277325
func (c *Client) IsReady() bool {
278326
c.mu.RLock()
@@ -314,6 +362,7 @@ func (c *Client) RefreshClient() error {
314362
c.discoveryClient = discoveryClient
315363
c.dynamicClient = dynamicClient
316364
c.clientset = clientset
365+
c.restConfig = config
317366

318367
return nil
319368
}

0 commit comments

Comments
 (0)