Skip to content

Commit 63166ea

Browse files
authored
test(mcp): improve denied resources tests (#480)
Required to allow for denial checks in a RoundTripper. Will enable creating a RESTConfig with a RoundTripper that checks for denied resources. Eventually, no kubernetes interface wrappers or clients will be needed. Signed-off-by: Marc Nuri <marc@marcnuri.com>
1 parent 0e88935 commit 63166ea

File tree

12 files changed

+125
-80
lines changed

12 files changed

+125
-80
lines changed

internal/test/mock_server.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,33 @@ WaitForStreams:
186186
return ctx, nil
187187
}
188188

189+
type DiscoveryClientHandler struct{}
190+
191+
var _ http.Handler = (*DiscoveryClientHandler)(nil)
192+
193+
func (h *DiscoveryClientHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
194+
// Request Performed by DiscoveryClient to Kube API (Get API Groups legacy -core-)
195+
if req.URL.Path == "/api" {
196+
w.Header().Set("Content-Type", "application/json")
197+
_, _ = w.Write([]byte(`{"kind":"APIVersions","versions":["v1"],"serverAddressByClientCIDRs":[{"clientCIDR":"0.0.0.0/0"}]}`))
198+
return
199+
}
200+
// Request Performed by DiscoveryClient to Kube API (Get API Groups)
201+
if req.URL.Path == "/apis" {
202+
w.Header().Set("Content-Type", "application/json")
203+
_, _ = w.Write([]byte(`{"kind":"APIGroupList","apiVersion":"v1","groups":[]}`))
204+
return
205+
}
206+
// Request Performed by DiscoveryClient to Kube API (Get API Resources)
207+
if req.URL.Path == "/api/v1" {
208+
w.Header().Set("Content-Type", "application/json")
209+
_, _ = w.Write([]byte(`{"kind":"APIResourceList","apiVersion":"v1","resources":[
210+
{"name":"pods","singularName":"","namespaced":true,"kind":"Pod","verbs":["get","list","watch","create","update","patch","delete"]}
211+
]}`))
212+
return
213+
}
214+
}
215+
189216
type InOpenShiftHandler struct {
190217
}
191218

pkg/mcp/events_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,10 @@ func (s *EventsSuite) TestEventsListDenied() {
132132
s.Nilf(err, "call tool should not return error object")
133133
})
134134
s.Run("describes denial", func() {
135-
expectedMessage := "failed to list events in all namespaces: resource not allowed: /v1, Kind=Event"
136-
s.Equalf(expectedMessage, toolResult.Content[0].(mcp.TextContent).Text,
135+
msg := toolResult.Content[0].(mcp.TextContent).Text
136+
s.Contains(msg, "resource not allowed:")
137+
expectedMessage := "failed to list events in all namespaces:(.+:)? resource not allowed: /v1, Kind=Event"
138+
s.Regexpf(expectedMessage, msg,
137139
"expected descriptive error '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text)
138140
})
139141
})

pkg/mcp/helm_test.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,11 @@ func (s *HelmSuite) TestHelmInstallDenied() {
8686
s.Nilf(err, "call tool should not return error object")
8787
})
8888
s.Run("describes denial", func() {
89-
s.Truef(strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "failed to install helm chart"), "expected descriptive error, got %v", toolResult.Content[0].(mcp.TextContent).Text)
89+
msg := toolResult.Content[0].(mcp.TextContent).Text
90+
s.Contains(msg, "resource not allowed:")
91+
s.Truef(strings.HasPrefix(msg, "failed to install helm chart"), "expected descriptive error, got %v", toolResult.Content[0].(mcp.TextContent).Text)
9092
expectedMessage := ": resource not allowed: /v1, Kind=Secret"
91-
s.Truef(strings.HasSuffix(toolResult.Content[0].(mcp.TextContent).Text, expectedMessage), "expected descriptive error '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text)
93+
s.Truef(strings.HasSuffix(msg, expectedMessage), "expected descriptive error '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text)
9294
})
9395
})
9496
}
@@ -260,8 +262,8 @@ func (s *HelmSuite) TestHelmUninstallDenied() {
260262
})
261263
s.Run("describes denial", func() {
262264
s.T().Skipf("Helm won't report what underlying resource caused the failure, so we can't assert on it")
263-
expectedMessage := "failed to uninstall release: resource not allowed: /v1, Kind=Secret"
264-
s.Equalf(expectedMessage, toolResult.Content[0].(mcp.TextContent).Text, "expected descriptive error '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text)
265+
expectedMessage := "failed to uninstall release:(.+:)? resource not allowed: /v1, Kind=Secret"
266+
s.Regexpf(expectedMessage, toolResult.Content[0].(mcp.TextContent).Text, "expected descriptive error '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text)
265267
})
266268
})
267269
}

pkg/mcp/mcp_test.go

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,25 +22,9 @@ func (s *McpHeadersSuite) SetupTest() {
2222
s.pathHeaders = make(map[string]http.Header)
2323
s.mockServer.Handle(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
2424
s.pathHeaders[req.URL.Path] = req.Header.Clone()
25-
// Request Performed by DiscoveryClient to Kube API (Get API Groups legacy -core-)
26-
if req.URL.Path == "/api" {
27-
w.Header().Set("Content-Type", "application/json")
28-
_, _ = w.Write([]byte(`{"kind":"APIVersions","versions":["v1"],"serverAddressByClientCIDRs":[{"clientCIDR":"0.0.0.0/0"}]}`))
29-
return
30-
}
31-
// Request Performed by DiscoveryClient to Kube API (Get API Groups)
32-
if req.URL.Path == "/apis" {
33-
w.Header().Set("Content-Type", "application/json")
34-
//w.Write([]byte(`{"kind":"APIGroupList","apiVersion":"v1","groups":[{"name":"apps","versions":[{"groupVersion":"apps/v1","version":"v1"}],"preferredVersion":{"groupVersion":"apps/v1","version":"v1"}}]}`))
35-
_, _ = w.Write([]byte(`{"kind":"APIGroupList","apiVersion":"v1","groups":[]}`))
36-
return
37-
}
38-
// Request Performed by DiscoveryClient to Kube API (Get API Resources)
39-
if req.URL.Path == "/api/v1" {
40-
w.Header().Set("Content-Type", "application/json")
41-
_, _ = w.Write([]byte(`{"kind":"APIResourceList","apiVersion":"v1","resources":[{"name":"pods","singularName":"","namespaced":true,"kind":"Pod","verbs":["get","list","watch","create","update","patch","delete"]}]}`))
42-
return
43-
}
25+
}))
26+
s.mockServer.Handle(&test.DiscoveryClientHandler{})
27+
s.mockServer.Handle(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
4428
// Request Performed by DynamicClient
4529
if req.URL.Path == "/api/v1/namespaces/default/pods" {
4630
w.Header().Set("Content-Type", "application/json")

pkg/mcp/namespaces_test.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@ func (s *NamespacesSuite) TestNamespacesListDenied() {
5656
s.Nilf(err, "call tool should not return error object")
5757
})
5858
s.Run("describes denial", func() {
59-
expectedMessage := "failed to list namespaces: resource not allowed: /v1, Kind=Namespace"
60-
s.Equalf(expectedMessage, toolResult.Content[0].(mcp.TextContent).Text,
59+
msg := toolResult.Content[0].(mcp.TextContent).Text
60+
s.Contains(msg, "resource not allowed:")
61+
expectedMessage := "failed to list namespaces:(.+:)? resource not allowed: /v1, Kind=Namespace"
62+
s.Regexpf(expectedMessage, msg,
6163
"expected descriptive error '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text)
6264
})
6365
})
@@ -159,8 +161,10 @@ func (s *NamespacesSuite) TestProjectsListInOpenShiftDenied() {
159161
s.Nilf(err, "call tool should not return error object")
160162
})
161163
s.Run("describes denial", func() {
162-
expectedMessage := "failed to list projects: resource not allowed: project.openshift.io/v1, Kind=Project"
163-
s.Equalf(expectedMessage, projectsList.Content[0].(mcp.TextContent).Text,
164+
msg := projectsList.Content[0].(mcp.TextContent).Text
165+
s.Contains(msg, "resource not allowed:")
166+
expectedMessage := "failed to list projects:(.+:)? resource not allowed: project.openshift.io/v1, Kind=Project"
167+
s.Regexpf(expectedMessage, msg,
164168
"expected descriptive error '%s', got %v", expectedMessage, projectsList.Content[0].(mcp.TextContent).Text)
165169
})
166170
})

pkg/mcp/nodes_test.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ func (s *NodesSuite) TestNodesLog() {
9393
})
9494
s.Run("describes missing name", func() {
9595
expectedMessage := "failed to get node log, missing argument query"
96-
s.Equalf(expectedMessage, toolResult.Content[0].(mcp.TextContent).Text,
96+
s.Regexpf(expectedMessage, toolResult.Content[0].(mcp.TextContent).Text,
9797
"expected descriptive error '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text)
9898
})
9999
})
@@ -215,8 +215,10 @@ func (s *NodesSuite) TestNodesLogDenied() {
215215
s.Nilf(err, "call tool should not return error object")
216216
})
217217
s.Run("describes denial", func() {
218+
msg := toolResult.Content[0].(mcp.TextContent).Text
219+
s.Contains(msg, "resource not allowed:")
218220
expectedMessage := "failed to get node log for does-not-matter: resource not allowed: /v1, Kind=Node"
219-
s.Equalf(expectedMessage, toolResult.Content[0].(mcp.TextContent).Text,
221+
s.Equalf(expectedMessage, msg,
220222
"expected descriptive error '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text)
221223
})
222224
})
@@ -324,8 +326,10 @@ func (s *NodesSuite) TestNodesStatsSummaryDenied() {
324326
s.Nilf(err, "call tool should not return error object")
325327
})
326328
s.Run("describes denial", func() {
327-
expectedMessage := "failed to get node stats summary for does-not-matter: resource not allowed: /v1, Kind=Node"
328-
s.Equalf(expectedMessage, toolResult.Content[0].(mcp.TextContent).Text,
329+
msg := toolResult.Content[0].(mcp.TextContent).Text
330+
s.Contains(msg, "resource not allowed:")
331+
expectedMessage := "failed to get node stats summary for does-not-matter:(.+:)? resource not allowed: /v1, Kind=Node"
332+
s.Regexpf(expectedMessage, msg,
329333
"expected descriptive error '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text)
330334
})
331335
})

pkg/mcp/nodes_top_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,8 +236,10 @@ func (s *NodesTopSuite) TestNodesTopDenied() {
236236
s.Nilf(err, "call tool should not return error object")
237237
})
238238
s.Run("describes denial", func() {
239-
expectedMessage := "failed to get nodes top: resource not allowed: metrics.k8s.io/v1beta1, Kind=NodeMetrics"
240-
s.Equalf(expectedMessage, toolResult.Content[0].(mcp.TextContent).Text,
239+
msg := toolResult.Content[0].(mcp.TextContent).Text
240+
s.Contains(msg, "resource not allowed:")
241+
expectedMessage := "failed to get nodes top:(.+:)? resource not allowed: metrics.k8s.io/v1beta1, Kind=NodeMetrics"
242+
s.Regexpf(expectedMessage, msg,
241243
"expected descriptive error '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text)
242244
})
243245
})

pkg/mcp/pods_exec_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,10 @@ func (s *PodsExecSuite) TestPodsExecDenied() {
126126
s.Nilf(err, "call tool should not return error object")
127127
})
128128
s.Run("describes denial", func() {
129-
expectedMessage := "failed to exec in pod pod-to-exec in namespace default: resource not allowed: /v1, Kind=Pod"
130-
s.Equalf(expectedMessage, toolResult.Content[0].(mcp.TextContent).Text,
129+
msg := toolResult.Content[0].(mcp.TextContent).Text
130+
s.Contains(msg, "resource not allowed:")
131+
expectedMessage := "failed to exec in pod pod-to-exec in namespace default:(.+:)? resource not allowed: /v1, Kind=Pod"
132+
s.Regexpf(expectedMessage, msg,
131133
"expected descriptive error '%s', got %v", expectedMessage, toolResult.Content[0].(mcp.TextContent).Text)
132134
})
133135
})

pkg/mcp/pods_run_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,10 @@ func (s *PodsRunSuite) TestPodsRunDenied() {
102102
s.Nilf(err, "call tool should not return error object")
103103
})
104104
s.Run("describes denial", func() {
105-
expectedMessage := "failed to run pod in namespace : resource not allowed: /v1, Kind=Pod"
106-
s.Equalf(expectedMessage, podsRun.Content[0].(mcp.TextContent).Text,
105+
msg := podsRun.Content[0].(mcp.TextContent).Text
106+
s.Contains(msg, "resource not allowed:")
107+
expectedMessage := "failed to run pod in namespace :(.+:)? resource not allowed: /v1, Kind=Pod"
108+
s.Regexpf(expectedMessage, msg,
107109
"expected descriptive error '%s', got %v", expectedMessage, podsRun.Content[0].(mcp.TextContent).Text)
108110
})
109111
})

pkg/mcp/pods_test.go

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,10 @@ func (s *PodsSuite) TestPodsListDenied() {
162162
s.Nilf(err, "call tool should not return error object")
163163
})
164164
s.Run("describes denial", func() {
165-
expectedMessage := "failed to list pods in all namespaces: resource not allowed: /v1, Kind=Pod"
166-
s.Equalf(expectedMessage, podsList.Content[0].(mcp.TextContent).Text,
165+
msg := podsList.Content[0].(mcp.TextContent).Text
166+
s.Contains(msg, "resource not allowed:")
167+
expectedMessage := "failed to list pods in all namespaces:(.+:)? resource not allowed: /v1, Kind=Pod"
168+
s.Regexpf(expectedMessage, msg,
167169
"expected descriptive error '%s', got %v", expectedMessage, podsList.Content[0].(mcp.TextContent).Text)
168170
})
169171
})
@@ -174,8 +176,10 @@ func (s *PodsSuite) TestPodsListDenied() {
174176
s.Nilf(err, "call tool should not return error object")
175177
})
176178
s.Run("describes denial", func() {
177-
expectedMessage := "failed to list pods in namespace ns-1: resource not allowed: /v1, Kind=Pod"
178-
s.Equalf(expectedMessage, podsListInNamespace.Content[0].(mcp.TextContent).Text,
179+
msg := podsListInNamespace.Content[0].(mcp.TextContent).Text
180+
s.Contains(msg, "resource not allowed:")
181+
expectedMessage := "failed to list pods in namespace ns-1:(.+:)? resource not allowed: /v1, Kind=Pod"
182+
s.Regexpf(expectedMessage, msg,
179183
"expected descriptive error '%s', got %v", expectedMessage, podsListInNamespace.Content[0].(mcp.TextContent).Text)
180184
})
181185
})
@@ -346,8 +350,10 @@ func (s *PodsSuite) TestPodsGetDenied() {
346350
s.Nilf(err, "call tool should not return error object")
347351
})
348352
s.Run("describes denial", func() {
349-
expectedMessage := "failed to get pod a-pod-in-default in namespace : resource not allowed: /v1, Kind=Pod"
350-
s.Equalf(expectedMessage, podsGet.Content[0].(mcp.TextContent).Text,
353+
msg := podsGet.Content[0].(mcp.TextContent).Text
354+
s.Contains(msg, "resource not allowed:")
355+
expectedMessage := "failed to get pod a-pod-in-default in namespace :(.+:)? resource not allowed: /v1, Kind=Pod"
356+
s.Regexpf(expectedMessage, msg,
351357
"expected descriptive error '%s', got %v", expectedMessage, podsGet.Content[0].(mcp.TextContent).Text)
352358
})
353359
})
@@ -447,8 +453,10 @@ func (s *PodsSuite) TestPodsDeleteDenied() {
447453
s.Nilf(err, "call tool should not return error object")
448454
})
449455
s.Run("describes denial", func() {
450-
expectedMessage := "failed to delete pod a-pod-in-default in namespace : resource not allowed: /v1, Kind=Pod"
451-
s.Equalf(expectedMessage, podsDelete.Content[0].(mcp.TextContent).Text,
456+
msg := podsDelete.Content[0].(mcp.TextContent).Text
457+
s.Contains(msg, "resource not allowed:")
458+
expectedMessage := "failed to delete pod a-pod-in-default in namespace :(.+:)? resource not allowed: /v1, Kind=Pod"
459+
s.Regexpf(expectedMessage, msg,
452460
"expected descriptive error '%s', got %v", expectedMessage, podsDelete.Content[0].(mcp.TextContent).Text)
453461
})
454462
})
@@ -599,8 +607,10 @@ func (s *PodsSuite) TestPodsLogDenied() {
599607
s.Nilf(err, "call tool should not return error object")
600608
})
601609
s.Run("describes denial", func() {
602-
expectedMessage := "failed to get pod a-pod-in-default log in namespace : resource not allowed: /v1, Kind=Pod"
603-
s.Equalf(expectedMessage, podsLog.Content[0].(mcp.TextContent).Text,
610+
msg := podsLog.Content[0].(mcp.TextContent).Text
611+
s.Contains(msg, "resource not allowed:")
612+
expectedMessage := "failed to get pod a-pod-in-default log in namespace :(.+:)? resource not allowed: /v1, Kind=Pod"
613+
s.Regexpf(expectedMessage, msg,
604614
"expected descriptive error '%s', got %v", expectedMessage, podsLog.Content[0].(mcp.TextContent).Text)
605615
})
606616
})

0 commit comments

Comments
 (0)