Skip to content

Commit eb6eb1c

Browse files
Rebase node_files and move the test to different file
Signed-off-by: Neeraj Krishna Gopalakrishna <ngopalak@redhat.com>
1 parent 738f88a commit eb6eb1c

File tree

2 files changed

+321
-280
lines changed

2 files changed

+321
-280
lines changed

pkg/mcp/nodes_files_test.go

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
package mcp
2+
3+
import (
4+
"net/http"
5+
"testing"
6+
7+
"github.com/BurntSushi/toml"
8+
"github.com/containers/kubernetes-mcp-server/internal/test"
9+
"github.com/mark3labs/mcp-go/mcp"
10+
"github.com/stretchr/testify/suite"
11+
)
12+
13+
type NodeFilesSuite struct {
14+
BaseMcpSuite
15+
mockServer *test.MockServer
16+
}
17+
18+
func (s *NodeFilesSuite) SetupTest() {
19+
s.BaseMcpSuite.SetupTest()
20+
s.mockServer = test.NewMockServer()
21+
s.Cfg.KubeConfig = s.mockServer.KubeconfigFile(s.T())
22+
s.mockServer.Handle(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
23+
w.Header().Set("Content-Type", "application/json")
24+
// Request Performed by DiscoveryClient to Kube API (Get API Groups legacy -core-)
25+
if req.URL.Path == "/api" {
26+
_, _ = w.Write([]byte(`{"kind":"APIVersions","versions":[],"serverAddressByClientCIDRs":[{"clientCIDR":"0.0.0.0/0"}]}`))
27+
return
28+
}
29+
}))
30+
}
31+
32+
func (s *NodeFilesSuite) TearDownTest() {
33+
s.BaseMcpSuite.TearDownTest()
34+
if s.mockServer != nil {
35+
s.mockServer.Close()
36+
}
37+
}
38+
39+
func (s *NodeFilesSuite) TestNodeFiles() {
40+
// Setup test files and directories
41+
s.T().Run("prepare test environment", func(t *testing.T) {
42+
// This ensures we have a node in the cluster for testing
43+
s.mockServer.Handle(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
44+
// Get Node response
45+
if req.URL.Path == "/api/v1/nodes/test-node" {
46+
w.Header().Set("Content-Type", "application/json")
47+
w.WriteHeader(http.StatusOK)
48+
_, _ = w.Write([]byte(`{
49+
"apiVersion": "v1",
50+
"kind": "Node",
51+
"metadata": {
52+
"name": "test-node"
53+
}
54+
}`))
55+
return
56+
}
57+
// Handle pod creation
58+
if req.URL.Path == "/api/v1/namespaces/default/pods" && req.Method == "POST" {
59+
w.Header().Set("Content-Type", "application/json")
60+
w.WriteHeader(http.StatusCreated)
61+
_, _ = w.Write([]byte(`{
62+
"apiVersion": "v1",
63+
"kind": "Pod",
64+
"metadata": {
65+
"name": "node-files-test",
66+
"namespace": "default"
67+
},
68+
"status": {
69+
"phase": "Running",
70+
"conditions": [{
71+
"type": "Ready",
72+
"status": "True"
73+
}]
74+
}
75+
}`))
76+
return
77+
}
78+
// Handle pod get (for wait)
79+
if req.URL.Path == "/api/v1/namespaces/default/pods/node-files-test" && req.Method == "GET" {
80+
w.Header().Set("Content-Type", "application/json")
81+
w.WriteHeader(http.StatusOK)
82+
_, _ = w.Write([]byte(`{
83+
"apiVersion": "v1",
84+
"kind": "Pod",
85+
"metadata": {
86+
"name": "node-files-test",
87+
"namespace": "default"
88+
},
89+
"status": {
90+
"phase": "Running",
91+
"conditions": [{
92+
"type": "Ready",
93+
"status": "True"
94+
}]
95+
}
96+
}`))
97+
return
98+
}
99+
w.WriteHeader(http.StatusNotFound)
100+
}))
101+
})
102+
103+
s.InitMcpClient()
104+
105+
// Test missing node_name parameter
106+
s.Run("node_files(node_name=nil)", func() {
107+
toolResult, err := s.CallTool("node_files", map[string]interface{}{
108+
"operation": "list",
109+
"source_path": "/tmp",
110+
})
111+
s.Require().NotNil(toolResult, "toolResult should not be nil")
112+
s.Run("has error", func() {
113+
s.Truef(toolResult.IsError, "call tool should fail")
114+
s.Nilf(err, "call tool should not return error object")
115+
})
116+
s.Run("describes missing node_name", func() {
117+
expectedMessage := "missing required argument: node_name"
118+
s.Equalf(expectedMessage, toolResult.Content[0].(mcp.TextContent).Text,
119+
"expected descriptive error '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text)
120+
})
121+
})
122+
123+
// Test missing operation parameter
124+
s.Run("node_files(operation=nil)", func() {
125+
toolResult, err := s.CallTool("node_files", map[string]interface{}{
126+
"node_name": "test-node",
127+
"source_path": "/tmp",
128+
})
129+
s.Require().NotNil(toolResult, "toolResult should not be nil")
130+
s.Run("has error", func() {
131+
s.Truef(toolResult.IsError, "call tool should fail")
132+
s.Nilf(err, "call tool should not return error object")
133+
})
134+
s.Run("describes missing operation", func() {
135+
expectedMessage := "missing required argument: operation"
136+
s.Equalf(expectedMessage, toolResult.Content[0].(mcp.TextContent).Text,
137+
"expected descriptive error '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text)
138+
})
139+
})
140+
141+
// Test missing source_path parameter
142+
s.Run("node_files(source_path=nil)", func() {
143+
toolResult, err := s.CallTool("node_files", map[string]interface{}{
144+
"node_name": "test-node",
145+
"operation": "list",
146+
})
147+
s.Require().NotNil(toolResult, "toolResult should not be nil")
148+
s.Run("has error", func() {
149+
s.Truef(toolResult.IsError, "call tool should fail")
150+
s.Nilf(err, "call tool should not return error object")
151+
})
152+
s.Run("describes missing source_path", func() {
153+
expectedMessage := "missing required argument: source_path"
154+
s.Equalf(expectedMessage, toolResult.Content[0].(mcp.TextContent).Text,
155+
"expected descriptive error '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text)
156+
})
157+
})
158+
159+
// Test invalid operation
160+
s.Run("node_files(operation=invalid)", func() {
161+
toolResult, err := s.CallTool("node_files", map[string]interface{}{
162+
"node_name": "test-node",
163+
"operation": "invalid",
164+
"source_path": "/tmp",
165+
})
166+
s.Require().NotNil(toolResult, "toolResult should not be nil")
167+
s.Run("has error", func() {
168+
s.Truef(toolResult.IsError, "call tool should fail")
169+
s.Nilf(err, "call tool should not return error object")
170+
})
171+
s.Run("describes invalid operation", func() {
172+
content := toolResult.Content[0].(mcp.TextContent).Text
173+
s.Containsf(content, "failed to perform node file operation", "expected error to mention failed operation, got %v", content)
174+
})
175+
})
176+
177+
// Test with non-existent node
178+
s.Run("node_files(node_name=non-existent-node)", func() {
179+
toolResult, err := s.CallTool("node_files", map[string]interface{}{
180+
"node_name": "non-existent-node",
181+
"operation": "list",
182+
"source_path": "/tmp",
183+
})
184+
s.Require().NotNil(toolResult, "toolResult should not be nil")
185+
s.Run("has error", func() {
186+
s.Truef(toolResult.IsError, "call tool should fail")
187+
s.Nilf(err, "call tool should not return error object")
188+
})
189+
s.Run("describes missing node", func() {
190+
content := toolResult.Content[0].(mcp.TextContent).Text
191+
s.Containsf(content, "failed to perform node file operation", "expected error to mention failed operation, got %v", content)
192+
})
193+
})
194+
195+
// Test with default namespace and image
196+
s.Run("node_files with defaults", func() {
197+
toolResult, _ := s.CallTool("node_files", map[string]interface{}{
198+
"node_name": "test-node",
199+
"operation": "list",
200+
"source_path": "/tmp",
201+
})
202+
s.Require().NotNil(toolResult, "toolResult should not be nil")
203+
// Note: This will fail in the mock environment, but we're testing parameter handling
204+
s.Run("attempts operation", func() {
205+
// The tool should attempt the operation even if it fails in mock environment
206+
s.NotNil(toolResult, "toolResult should not be nil")
207+
})
208+
})
209+
210+
// Test with custom namespace
211+
s.Run("node_files with custom namespace", func() {
212+
toolResult, _ := s.CallTool("node_files", map[string]interface{}{
213+
"node_name": "test-node",
214+
"operation": "list",
215+
"source_path": "/tmp",
216+
"namespace": "custom-ns",
217+
})
218+
s.Require().NotNil(toolResult, "toolResult should not be nil")
219+
// The operation will fail in mock environment, but we're verifying parameters are passed
220+
})
221+
222+
// Test with custom image
223+
s.Run("node_files with custom image", func() {
224+
toolResult, _ := s.CallTool("node_files", map[string]interface{}{
225+
"node_name": "test-node",
226+
"operation": "list",
227+
"source_path": "/tmp",
228+
"image": "alpine",
229+
})
230+
s.Require().NotNil(toolResult, "toolResult should not be nil")
231+
// The operation will fail in mock environment, but we're verifying parameters are passed
232+
s.NotNil(toolResult)
233+
})
234+
235+
// Test with privileged=false
236+
s.Run("node_files with privileged=false", func() {
237+
toolResult, _ := s.CallTool("node_files", map[string]interface{}{
238+
"node_name": "test-node",
239+
"operation": "list",
240+
"source_path": "/tmp",
241+
"privileged": false,
242+
})
243+
s.Require().NotNil(toolResult, "toolResult should not be nil")
244+
// The operation will fail in mock environment, but we're verifying parameters are passed
245+
s.NotNil(toolResult)
246+
})
247+
248+
// Test list operation
249+
s.Run("node_files operation=list", func() {
250+
toolResult, _ := s.CallTool("node_files", map[string]interface{}{
251+
"node_name": "test-node",
252+
"operation": "list",
253+
"source_path": "/proc",
254+
})
255+
s.Require().NotNil(toolResult, "toolResult should not be nil")
256+
// Will fail in mock environment but tests the operation type
257+
})
258+
259+
// Test get operation
260+
s.Run("node_files operation=get", func() {
261+
toolResult, _ := s.CallTool("node_files", map[string]interface{}{
262+
"node_name": "test-node",
263+
"operation": "get",
264+
"source_path": "/proc/cpuinfo",
265+
"dest_path": "/tmp/cpuinfo",
266+
})
267+
s.Require().NotNil(toolResult, "toolResult should not be nil")
268+
// Will fail in mock environment but tests the operation type
269+
})
270+
271+
// Test get operation without dest_path
272+
s.Run("node_files operation=get without dest_path", func() {
273+
toolResult, _ := s.CallTool("node_files", map[string]interface{}{
274+
"node_name": "test-node",
275+
"operation": "get",
276+
"source_path": "/proc/meminfo",
277+
})
278+
s.Require().NotNil(toolResult, "toolResult should not be nil")
279+
// Will fail in mock environment but tests the operation type
280+
})
281+
282+
// Test put operation
283+
s.Run("node_files operation=put", func() {
284+
toolResult, _ := s.CallTool("node_files", map[string]interface{}{
285+
"node_name": "test-node",
286+
"operation": "put",
287+
"source_path": "/tmp/local-file",
288+
"dest_path": "/tmp/node-file",
289+
})
290+
s.Require().NotNil(toolResult, "toolResult should not be nil")
291+
// Will fail in mock environment but tests the operation type
292+
})
293+
}
294+
295+
func (s *NodeFilesSuite) TestNodeFilesDenied() {
296+
s.Require().NoError(toml.Unmarshal([]byte(`
297+
denied_resources = [ { version = "v1", kind = "Pod" } ]
298+
`), s.Cfg), "Expected to parse denied resources config")
299+
s.InitMcpClient()
300+
s.Run("node_files (denied)", func() {
301+
toolResult, err := s.CallTool("node_files", map[string]interface{}{
302+
"node_name": "test-node",
303+
"operation": "list",
304+
"source_path": "/tmp",
305+
})
306+
s.Require().NotNil(toolResult, "toolResult should not be nil")
307+
s.Run("has error", func() {
308+
s.Truef(toolResult.IsError, "call tool should fail")
309+
s.Nilf(err, "call tool should not return error object")
310+
})
311+
s.Run("describes denial", func() {
312+
expectedMessage := "failed to perform node file operation: resource not allowed: /v1, Kind=Pod"
313+
s.Containsf(toolResult.Content[0].(mcp.TextContent).Text, "resource not allowed",
314+
"expected descriptive error '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text)
315+
})
316+
})
317+
}
318+
319+
func TestNodeFiles(t *testing.T) {
320+
suite.Run(t, new(NodeFilesSuite))
321+
}

0 commit comments

Comments
 (0)