@@ -7,125 +7,132 @@ import (
77 "strings"
88 "testing"
99
10+ "github.com/BurntSushi/toml"
1011 "github.com/mark3labs/mcp-go/mcp"
12+ "github.com/stretchr/testify/suite"
1113 v1 "k8s.io/api/core/v1"
1214 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1315
1416 "github.com/containers/kubernetes-mcp-server/internal/test"
15- "github.com/containers/kubernetes-mcp-server/pkg/config"
1617)
1718
18- func TestPodsExec (t * testing.T ) {
19- testCase (t , func (c * mcpContext ) {
20- mockServer := test .NewMockServer ()
21- defer mockServer .Close ()
22- c .withKubeConfig (mockServer .Config ())
23- mockServer .Handle (http .HandlerFunc (func (w http.ResponseWriter , req * http.Request ) {
24- if req .URL .Path != "/api/v1/namespaces/default/pods/pod-to-exec/exec" {
25- return
26- }
27- var stdin , stdout bytes.Buffer
28- ctx , err := test .CreateHTTPStreams (w , req , & test.StreamOptions {
29- Stdin : & stdin ,
30- Stdout : & stdout ,
31- })
32- if err != nil {
33- w .WriteHeader (http .StatusInternalServerError )
34- _ , _ = w .Write ([]byte (err .Error ()))
35- return
36- }
37- defer func (conn io.Closer ) { _ = conn .Close () }(ctx .Closer )
38- _ , _ = io .WriteString (ctx .StdoutStream , "command:" + strings .Join (req .URL .Query ()["command" ], " " )+ "\n " )
39- _ , _ = io .WriteString (ctx .StdoutStream , "container:" + strings .Join (req .URL .Query ()["container" ], " " )+ "\n " )
40- }))
41- mockServer .Handle (http .HandlerFunc (func (w http.ResponseWriter , req * http.Request ) {
42- if req .URL .Path != "/api/v1/namespaces/default/pods/pod-to-exec" {
43- return
44- }
45- test .WriteObject (w , & v1.Pod {
46- ObjectMeta : metav1.ObjectMeta {
47- Namespace : "default" ,
48- Name : "pod-to-exec" ,
49- },
50- Spec : v1.PodSpec {Containers : []v1.Container {{Name : "container-to-exec" }}},
51- })
52- }))
53- podsExecNilNamespace , err := c .callTool ("pods_exec" , map [string ]interface {}{
19+ type PodsExecSuite struct {
20+ BaseMcpSuite
21+ mockServer * test.MockServer
22+ }
23+
24+ func (s * PodsExecSuite ) SetupTest () {
25+ s .BaseMcpSuite .SetupTest ()
26+ s .mockServer = test .NewMockServer ()
27+ s .Cfg .KubeConfig = s .mockServer .KubeconfigFile (s .T ())
28+ }
29+
30+ func (s * PodsExecSuite ) TearDownTest () {
31+ s .BaseMcpSuite .TearDownTest ()
32+ if s .mockServer != nil {
33+ s .mockServer .Close ()
34+ }
35+ }
36+
37+ func (s * PodsExecSuite ) TestPodsExec () {
38+ s .mockServer .Handle (http .HandlerFunc (func (w http.ResponseWriter , req * http.Request ) {
39+ if req .URL .Path != "/api/v1/namespaces/default/pods/pod-to-exec/exec" {
40+ return
41+ }
42+ var stdin , stdout bytes.Buffer
43+ ctx , err := test .CreateHTTPStreams (w , req , & test.StreamOptions {
44+ Stdin : & stdin ,
45+ Stdout : & stdout ,
46+ })
47+ if err != nil {
48+ w .WriteHeader (http .StatusInternalServerError )
49+ _ , _ = w .Write ([]byte (err .Error ()))
50+ return
51+ }
52+ defer func (conn io.Closer ) { _ = conn .Close () }(ctx .Closer )
53+ _ , _ = io .WriteString (ctx .StdoutStream , "command:" + strings .Join (req .URL .Query ()["command" ], " " )+ "\n " )
54+ _ , _ = io .WriteString (ctx .StdoutStream , "container:" + strings .Join (req .URL .Query ()["container" ], " " )+ "\n " )
55+ }))
56+ s .mockServer .Handle (http .HandlerFunc (func (w http.ResponseWriter , req * http.Request ) {
57+ if req .URL .Path != "/api/v1/namespaces/default/pods/pod-to-exec" {
58+ return
59+ }
60+ test .WriteObject (w , & v1.Pod {
61+ ObjectMeta : metav1.ObjectMeta {
62+ Namespace : "default" ,
63+ Name : "pod-to-exec" ,
64+ },
65+ Spec : v1.PodSpec {Containers : []v1.Container {{Name : "container-to-exec" }}},
66+ })
67+ }))
68+ s .InitMcpClient ()
69+
70+ s .Run ("pods_exec(name=pod-to-exec, command=[ls -l])" , func () {
71+ result , err := s .CallTool ("pods_exec" , map [string ]interface {}{
5472 "name" : "pod-to-exec" ,
5573 "command" : []interface {}{"ls" , "-l" },
5674 })
57- t .Run ("pods_exec with name and nil namespace returns command output" , func (t * testing.T ) {
58- if err != nil {
59- t .Fatalf ("call tool failed %v" , err )
60- }
61- if podsExecNilNamespace .IsError {
62- t .Fatalf ("call tool failed: %v" , podsExecNilNamespace .Content )
63- }
64- if ! strings .Contains (podsExecNilNamespace .Content [0 ].(mcp.TextContent ).Text , "command:ls -l\n " ) {
65- t .Errorf ("unexpected result %v" , podsExecNilNamespace .Content [0 ].(mcp.TextContent ).Text )
66- }
75+ s .Require ().NotNil (result )
76+ s .Run ("returns command output" , func () {
77+ s .NoError (err , "call tool failed %v" , err )
78+ s .Falsef (result .IsError , "call tool failed: %v" , result .Content )
79+ s .Contains (result .Content [0 ].(mcp.TextContent ).Text , "command:ls -l\n " , "unexpected result %v" , result .Content [0 ].(mcp.TextContent ).Text )
6780 })
68- podsExecInNamespace , err := c .callTool ("pods_exec" , map [string ]interface {}{
81+ })
82+ s .Run ("pods_exec(name=pod-to-exec, namespace=default, command=[ls -l])" , func () {
83+ result , err := s .CallTool ("pods_exec" , map [string ]interface {}{
6984 "namespace" : "default" ,
7085 "name" : "pod-to-exec" ,
7186 "command" : []interface {}{"ls" , "-l" },
7287 })
73- t .Run ("pods_exec with name and namespace returns command output" , func (t * testing.T ) {
74- if err != nil {
75- t .Fatalf ("call tool failed %v" , err )
76- }
77- if podsExecInNamespace .IsError {
78- t .Fatalf ("call tool failed: %v" , podsExecInNamespace .Content )
79- }
80- if ! strings .Contains (podsExecInNamespace .Content [0 ].(mcp.TextContent ).Text , "command:ls -l\n " ) {
81- t .Errorf ("unexpected result %v" , podsExecInNamespace .Content [0 ].(mcp.TextContent ).Text )
82- }
88+ s .Require ().NotNil (result )
89+ s .Run ("returns command output" , func () {
90+ s .NoError (err , "call tool failed %v" , err )
91+ s .Falsef (result .IsError , "call tool failed: %v" , result .Content )
92+ s .Contains (result .Content [0 ].(mcp.TextContent ).Text , "command:ls -l\n " , "unexpected result %v" , result .Content [0 ].(mcp.TextContent ).Text )
8393 })
84- podsExecInNamespaceAndContainer , err := c .callTool ("pods_exec" , map [string ]interface {}{
94+ })
95+ s .Run ("pods_exec(name=pod-to-exec, namespace=default, command=[ls -l], container=a-specific-container)" , func () {
96+ result , err := s .CallTool ("pods_exec" , map [string ]interface {}{
8597 "namespace" : "default" ,
8698 "name" : "pod-to-exec" ,
8799 "command" : []interface {}{"ls" , "-l" },
88100 "container" : "a-specific-container" ,
89101 })
90- t .Run ("pods_exec with name, namespace, and container returns command output" , func (t * testing.T ) {
91- if err != nil {
92- t .Fatalf ("call tool failed %v" , err )
93- }
94- if podsExecInNamespaceAndContainer .IsError {
95- t .Fatalf ("call tool failed" )
96- }
97- if ! strings .Contains (podsExecInNamespaceAndContainer .Content [0 ].(mcp.TextContent ).Text , "command:ls -l\n " ) {
98- t .Errorf ("unexpected result %v" , podsExecInNamespaceAndContainer .Content [0 ].(mcp.TextContent ).Text )
99- }
100- if ! strings .Contains (podsExecInNamespaceAndContainer .Content [0 ].(mcp.TextContent ).Text , "container:a-specific-container\n " ) {
101- t .Errorf ("expected container name not found %v" , podsExecInNamespaceAndContainer .Content [0 ].(mcp.TextContent ).Text )
102- }
102+ s .Require ().NotNil (result )
103+ s .Run ("returns command output" , func () {
104+ s .NoError (err , "call tool failed %v" , err )
105+ s .Falsef (result .IsError , "call tool failed: %v" , result .Content )
106+ s .Contains (result .Content [0 ].(mcp.TextContent ).Text , "command:ls -l\n " , "unexpected result %v" , result .Content [0 ].(mcp.TextContent ).Text )
103107 })
104108 })
105109}
106110
107- func TestPodsExecDenied ( t * testing. T ) {
108- deniedResourcesServer := test . Must ( config . ReadToml ([]byte (`
111+ func ( s * PodsExecSuite ) TestPodsExecDenied ( ) {
112+ s . Require (). NoError ( toml . Unmarshal ([]byte (`
109113 denied_resources = [ { version = "v1", kind = "Pod" } ]
110- ` )) )
111- testCaseWithContext ( t , & mcpContext { staticConfig : deniedResourcesServer }, func ( c * mcpContext ) {
112- c . withEnvTest ()
113- podsRun , _ := c . callTool ("pods_exec" , map [string ]interface {}{
114+ ` ), s . Cfg ), "Expected to parse denied resources config" )
115+ s . InitMcpClient ()
116+ s . Run ( "pods_exec (denied)" , func () {
117+ toolResult , err := s . CallTool ("pods_exec" , map [string ]interface {}{
114118 "namespace" : "default" ,
115119 "name" : "pod-to-exec" ,
116120 "command" : []interface {}{"ls" , "-l" },
117121 "container" : "a-specific-container" ,
118122 })
119- t . Run ( "pods_exec has error" , func ( t * testing. T ) {
120- if ! podsRun . IsError {
121- t . Fatalf ( "call tool should fail" )
122- }
123+ s . Require (). NotNil ( toolResult , "toolResult should not be nil" )
124+ s . Run ( "has error" , func () {
125+ s . Truef ( toolResult . IsError , "call tool should fail" )
126+ s . Nilf ( err , "call tool should not return error object" )
123127 })
124- t .Run ("pods_exec describes denial" , func (t * testing. T ) {
128+ s .Run ("describes denial" , func () {
125129 expectedMessage := "failed to exec in pod pod-to-exec in namespace default: resource not allowed: /v1, Kind=Pod"
126- if podsRun .Content [0 ].(mcp.TextContent ).Text != expectedMessage {
127- t .Fatalf ("expected descriptive error '%s', got %v" , expectedMessage , podsRun .Content [0 ].(mcp.TextContent ).Text )
128- }
130+ s .Equalf (expectedMessage , toolResult .Content [0 ].(mcp.TextContent ).Text ,
131+ "expected descriptive error '%s', got %v" , expectedMessage , toolResult .Content [0 ].(mcp.TextContent ).Text )
129132 })
130133 })
131134}
135+
136+ func TestPodsExec (t * testing.T ) {
137+ suite .Run (t , new (PodsExecSuite ))
138+ }
0 commit comments