Skip to content

test(config): extensive test suite for denied lists #148

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 3 additions & 18 deletions pkg/mcp/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,23 +124,12 @@ func (c *mcpContext) beforeEach(t *testing.T) {
if c.listOutput == nil {
c.listOutput = output.Yaml
}
if c.staticConfig == nil {
c.staticConfig = &config.StaticConfig{}
}
if c.before != nil {
c.before(c)
}
if c.staticConfig == nil {
c.staticConfig = &config.StaticConfig{
DeniedResources: []config.GroupVersionKind{
{
Version: "v1",
Kind: "Secret",
},
{
Group: "rbac.authorization.k8s.io",
Version: "v1",
},
},
}
}
if c.mcpServer, err = NewServer(Configuration{
Profile: c.profile,
ListOutput: c.listOutput,
Expand Down Expand Up @@ -222,10 +211,6 @@ func (c *mcpContext) withKubeConfig(rc *rest.Config) *api.Config {
return fakeConfig
}

func (c *mcpContext) withStaticConfig(config *config.StaticConfig) {
c.staticConfig = config
}

// withEnvTest sets up the environment for kubeconfig to be used with envTest
func (c *mcpContext) withEnvTest() {
c.withKubeConfig(envTestRestConfig)
Expand Down
20 changes: 20 additions & 0 deletions pkg/mcp/events_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package mcp

import (
"github.com/manusa/kubernetes-mcp-server/pkg/config"
"github.com/mark3labs/mcp-go/mcp"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -93,3 +94,22 @@ func TestEventsList(t *testing.T) {
})
})
}

func TestEventsListDenied(t *testing.T) {
deniedResourcesServer := &config.StaticConfig{DeniedResources: []config.GroupVersionKind{{Version: "v1", Kind: "Event"}}}
testCaseWithContext(t, &mcpContext{staticConfig: deniedResourcesServer}, func(c *mcpContext) {
c.withEnvTest()
eventList, _ := c.callTool("events_list", map[string]interface{}{})
t.Run("events_list has error", func(t *testing.T) {
if !eventList.IsError {
t.Fatalf("call tool should fail")
}
})
t.Run("events_list describes denial", func(t *testing.T) {
expectedMessage := "failed to list events in all namespaces: resource not allowed: /v1, Kind=Event"
if eventList.Content[0].(mcp.TextContent).Text != expectedMessage {
t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, eventList.Content[0].(mcp.TextContent).Text)
}
})
})
}
29 changes: 29 additions & 0 deletions pkg/mcp/helm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package mcp
import (
"context"
"encoding/base64"
"github.com/manusa/kubernetes-mcp-server/pkg/config"
"github.com/mark3labs/mcp-go/mcp"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
Expand Down Expand Up @@ -57,6 +58,30 @@ func TestHelmInstall(t *testing.T) {
})
}

func TestHelmInstallDenied(t *testing.T) {
t.Skip("To be implemented") // TODO: helm_install is not checking for denied resources
deniedResourcesServer := &config.StaticConfig{DeniedResources: []config.GroupVersionKind{{Version: "v1", Kind: "Secret"}}}
testCaseWithContext(t, &mcpContext{staticConfig: deniedResourcesServer}, func(c *mcpContext) {
c.withEnvTest()
_, file, _, _ := runtime.Caller(0)
chartPath := filepath.Join(filepath.Dir(file), "testdata", "helm-chart-secret")
helmInstall, _ := c.callTool("helm_install", map[string]interface{}{
"chart": chartPath,
})
t.Run("helm_install has error", func(t *testing.T) {
if !helmInstall.IsError {
t.Fatalf("call tool should fail")
}
})
t.Run("helm_install describes denial", func(t *testing.T) {
expectedMessage := "failed to install helm chart: resource not allowed: /v1, Kind=Secret"
if helmInstall.Content[0].(mcp.TextContent).Text != expectedMessage {
t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, helmInstall.Content[0].(mcp.TextContent).Text)
}
})
})
}

func TestHelmList(t *testing.T) {
testCase(t, func(c *mcpContext) {
c.withEnvTest()
Expand Down Expand Up @@ -199,6 +224,10 @@ func TestHelmUninstall(t *testing.T) {
})
}

func TestHelmUninstallDenied(t *testing.T) {
t.Skip("To be implemented") // TODO: helm_uninstall is not checking for denied resources
}

func clearHelmReleases(ctx context.Context, kc *kubernetes.Clientset) {
secrets, _ := kc.CoreV1().Secrets("default").List(ctx, metav1.ListOptions{})
for _, secret := range secrets.Items {
Expand Down
3 changes: 0 additions & 3 deletions pkg/mcp/mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import (

"github.com/mark3labs/mcp-go/client"
"github.com/mark3labs/mcp-go/mcp"

"github.com/manusa/kubernetes-mcp-server/pkg/config"
)

func TestWatchKubeConfig(t *testing.T) {
Expand Down Expand Up @@ -99,7 +97,6 @@ func TestSseHeaders(t *testing.T) {
defer mockServer.Close()
before := func(c *mcpContext) {
c.withKubeConfig(mockServer.config)
c.withStaticConfig(&config.StaticConfig{})
c.clientOptions = append(c.clientOptions, client.WithHeaders(map[string]string{"kubernetes-authorization": "Bearer a-token-from-mcp-client"}))
}
pathHeaders := make(map[string]http.Header, 0)
Expand Down
39 changes: 39 additions & 0 deletions pkg/mcp/namespaces_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package mcp

import (
"github.com/manusa/kubernetes-mcp-server/pkg/config"
"github.com/manusa/kubernetes-mcp-server/pkg/output"
"github.com/mark3labs/mcp-go/mcp"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -48,6 +49,25 @@ func TestNamespacesList(t *testing.T) {
})
}

func TestNamespacesListDenied(t *testing.T) {
deniedResourcesServer := &config.StaticConfig{DeniedResources: []config.GroupVersionKind{{Version: "v1", Kind: "Namespace"}}}
testCaseWithContext(t, &mcpContext{staticConfig: deniedResourcesServer}, func(c *mcpContext) {
c.withEnvTest()
namespacesList, _ := c.callTool("namespaces_list", map[string]interface{}{})
t.Run("namespaces_list has error", func(t *testing.T) {
if !namespacesList.IsError {
t.Fatalf("call tool should fail")
}
})
t.Run("namespaces_list describes denial", func(t *testing.T) {
expectedMessage := "failed to list namespaces: resource not allowed: /v1, Kind=Namespace"
if namespacesList.Content[0].(mcp.TextContent).Text != expectedMessage {
t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, namespacesList.Content[0].(mcp.TextContent).Text)
}
})
})
}

func TestNamespacesListAsTable(t *testing.T) {
testCaseWithContext(t, &mcpContext{listOutput: output.Table}, func(c *mcpContext) {
c.withEnvTest()
Expand Down Expand Up @@ -133,3 +153,22 @@ func TestProjectsListInOpenShift(t *testing.T) {
})
})
}

func TestProjectsListInOpenShiftDenied(t *testing.T) {
deniedResourcesServer := &config.StaticConfig{DeniedResources: []config.GroupVersionKind{{Group: "project.openshift.io", Version: "v1"}}}
testCaseWithContext(t, &mcpContext{staticConfig: deniedResourcesServer, before: inOpenShift, after: inOpenShiftClear}, func(c *mcpContext) {
c.withEnvTest()
projectsList, _ := c.callTool("projects_list", map[string]interface{}{})
t.Run("projects_list has error", func(t *testing.T) {
if !projectsList.IsError {
t.Fatalf("call tool should fail")
}
})
t.Run("projects_list describes denial", func(t *testing.T) {
expectedMessage := "failed to list projects: resource not allowed: project.openshift.io/v1, Kind=Project"
if projectsList.Content[0].(mcp.TextContent).Text != expectedMessage {
t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, projectsList.Content[0].(mcp.TextContent).Text)
}
})
})
}
2 changes: 1 addition & 1 deletion pkg/mcp/pods.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ func (s *Server) podsRun(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.Cal
}
resources, err := s.k.Derived(ctx).PodsRun(ctx, ns.(string), name.(string), image.(string), int32(port.(float64)))
if err != nil {
return NewTextResult("", fmt.Errorf("failed to get pod %s log in namespace %s: %v", name, ns, err)), nil
return NewTextResult("", fmt.Errorf("failed to run pod %s in namespace %s: %v", name, ns, err)), nil
}
marshalledYaml, err := output.MarshalYaml(resources)
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions pkg/mcp/pods_exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,7 @@ func TestPodsExec(t *testing.T) {
})
})
}

func TestPodsExecDenied(t *testing.T) {
t.Skip("To be implemented") // TODO: exec is not checking for denied resources
}
110 changes: 110 additions & 0 deletions pkg/mcp/pods_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package mcp

import (
"github.com/manusa/kubernetes-mcp-server/pkg/config"
"github.com/manusa/kubernetes-mcp-server/pkg/output"
"regexp"
"strings"
Expand Down Expand Up @@ -176,6 +177,37 @@ func TestPodsListInNamespace(t *testing.T) {
})
}

func TestPodsListDenied(t *testing.T) {
deniedResourcesServer := &config.StaticConfig{DeniedResources: []config.GroupVersionKind{{Version: "v1", Kind: "Pod"}}}
testCaseWithContext(t, &mcpContext{staticConfig: deniedResourcesServer}, func(c *mcpContext) {
c.withEnvTest()
podsList, _ := c.callTool("pods_list", map[string]interface{}{})
t.Run("pods_list has error", func(t *testing.T) {
if !podsList.IsError {
t.Fatalf("call tool should fail")
}
})
t.Run("pods_list describes denial", func(t *testing.T) {
expectedMessage := "failed to list pods in all namespaces: resource not allowed: /v1, Kind=Pod"
if podsList.Content[0].(mcp.TextContent).Text != expectedMessage {
t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, podsList.Content[0].(mcp.TextContent).Text)
}
})
podsListInNamespace, _ := c.callTool("pods_list_in_namespace", map[string]interface{}{"namespace": "ns-1"})
t.Run("pods_list_in_namespace has error", func(t *testing.T) {
if !podsListInNamespace.IsError {
t.Fatalf("call tool should fail")
}
})
t.Run("pods_list_in_namespace describes denial", func(t *testing.T) {
expectedMessage := "failed to list pods in namespace ns-1: resource not allowed: /v1, Kind=Pod"
if podsListInNamespace.Content[0].(mcp.TextContent).Text != expectedMessage {
t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, podsListInNamespace.Content[0].(mcp.TextContent).Text)
}
})
})
}

func TestPodsListAsTable(t *testing.T) {
testCaseWithContext(t, &mcpContext{listOutput: output.Table}, func(c *mcpContext) {
c.withEnvTest()
Expand Down Expand Up @@ -380,6 +412,25 @@ func TestPodsGet(t *testing.T) {
})
}

func TestPodsGetDenied(t *testing.T) {
deniedResourcesServer := &config.StaticConfig{DeniedResources: []config.GroupVersionKind{{Version: "v1", Kind: "Pod"}}}
testCaseWithContext(t, &mcpContext{staticConfig: deniedResourcesServer}, func(c *mcpContext) {
c.withEnvTest()
podsGet, _ := c.callTool("pods_get", map[string]interface{}{"name": "a-pod-in-default"})
t.Run("pods_get has error", func(t *testing.T) {
if !podsGet.IsError {
t.Fatalf("call tool should fail")
}
})
t.Run("pods_get describes denial", func(t *testing.T) {
expectedMessage := "failed to get pod a-pod-in-default in namespace : resource not allowed: /v1, Kind=Pod"
if podsGet.Content[0].(mcp.TextContent).Text != expectedMessage {
t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, podsGet.Content[0].(mcp.TextContent).Text)
}
})
})
}

func TestPodsDelete(t *testing.T) {
testCase(t, func(c *mcpContext) {
c.withEnvTest()
Expand Down Expand Up @@ -511,6 +562,26 @@ func TestPodsDelete(t *testing.T) {
})
}

func TestPodsDeleteDenied(t *testing.T) {
t.Skip("To be implemented") // TODO: delete is not checking for denied resources
deniedResourcesServer := &config.StaticConfig{DeniedResources: []config.GroupVersionKind{{Version: "v1", Kind: "Pod"}}}
testCaseWithContext(t, &mcpContext{staticConfig: deniedResourcesServer}, func(c *mcpContext) {
c.withEnvTest()
podsDelete, _ := c.callTool("pods_delete", map[string]interface{}{"name": "a-pod-in-default"})
t.Run("pods_delete has error", func(t *testing.T) {
if !podsDelete.IsError {
t.Fatalf("call tool should fail")
}
})
t.Run("pods_delete describes denial", func(t *testing.T) {
expectedMessage := "failed to delete pod a-pod-in-default in namespace : resource not allowed: /v1, Kind=Pod"
if podsDelete.Content[0].(mcp.TextContent).Text != expectedMessage {
t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, podsDelete.Content[0].(mcp.TextContent).Text)
}
})
})
}

func TestPodsDeleteInOpenShift(t *testing.T) {
testCaseWithContext(t, &mcpContext{before: inOpenShift, after: inOpenShiftClear}, func(c *mcpContext) {
managedLabels := map[string]string{
Expand Down Expand Up @@ -651,6 +722,26 @@ func TestPodsLog(t *testing.T) {
})
}

func TestPodsLogDenied(t *testing.T) {
t.Skip("To be implemented") // TODO: log is not checking for denied resources
deniedResourcesServer := &config.StaticConfig{DeniedResources: []config.GroupVersionKind{{Version: "v1", Kind: "Pod"}}}
testCaseWithContext(t, &mcpContext{staticConfig: deniedResourcesServer}, func(c *mcpContext) {
c.withEnvTest()
podsLog, _ := c.callTool("pods_log", map[string]interface{}{"name": "a-pod-in-default"})
t.Run("pods_log has error", func(t *testing.T) {
if !podsLog.IsError {
t.Fatalf("call tool should fail")
}
})
t.Run("pods_log describes denial", func(t *testing.T) {
expectedMessage := "failed to log pod a-pod-in-default in namespace : resource not allowed: /v1, Kind=Pod"
if podsLog.Content[0].(mcp.TextContent).Text != expectedMessage {
t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, podsLog.Content[0].(mcp.TextContent).Text)
}
})
})
}

func TestPodsRun(t *testing.T) {
testCase(t, func(c *mcpContext) {
c.withEnvTest()
Expand Down Expand Up @@ -801,6 +892,25 @@ func TestPodsRun(t *testing.T) {
})
}

func TestPodsRunDenied(t *testing.T) {
deniedResourcesServer := &config.StaticConfig{DeniedResources: []config.GroupVersionKind{{Version: "v1", Kind: "Pod"}}}
testCaseWithContext(t, &mcpContext{staticConfig: deniedResourcesServer}, func(c *mcpContext) {
c.withEnvTest()
podsRun, _ := c.callTool("pods_run", map[string]interface{}{"image": "nginx"})
t.Run("pods_run has error", func(t *testing.T) {
if !podsRun.IsError {
t.Fatalf("call tool should fail")
}
})
t.Run("pods_run describes denial", func(t *testing.T) {
expectedMessage := "failed to run pod in namespace : resource not allowed: /v1, Kind=Pod"
if podsRun.Content[0].(mcp.TextContent).Text != expectedMessage {
t.Fatalf("expected desciptive error '%s', got %v", expectedMessage, podsRun.Content[0].(mcp.TextContent).Text)
}
})
})
}

func TestPodsRunInOpenShift(t *testing.T) {
testCaseWithContext(t, &mcpContext{before: inOpenShift, after: inOpenShiftClear}, func(c *mcpContext) {
t.Run("pods_run with image, namespace, and port returns route with port", func(t *testing.T) {
Expand Down
4 changes: 4 additions & 0 deletions pkg/mcp/pods_top_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,7 @@ func TestPodsTopMetricsAvailable(t *testing.T) {
})
})
}

func TestPodsTopDenied(t *testing.T) {
t.Skip("To be implemented") // TODO: top is not checking for denied resources
}
Loading
Loading