Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/http/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ func (c *httpContext) beforeEach(t *testing.T) {
mockKubeConfig := c.mockServer.KubeConfig()
kubeConfig := filepath.Join(t.TempDir(), "config")
_ = clientcmd.WriteToFile(*mockKubeConfig, kubeConfig)
_ = os.Setenv("KUBECONFIG", kubeConfig)
// Capture logging
c.klogState = klog.CaptureState()
flags := flag.NewFlagSet("test", flag.ContinueOnError)
Expand All @@ -86,6 +85,7 @@ func (c *httpContext) beforeEach(t *testing.T) {
t.Fatalf("Failed to close random port listener: %v", randomPortErr)
}
c.StaticConfig.Port = fmt.Sprintf("%d", ln.Addr().(*net.TCPAddr).Port)
c.StaticConfig.KubeConfig = kubeConfig
mcpServer, err := mcp.NewServer(mcp.Configuration{
Profile: mcp.Profiles[0],
StaticConfig: c.StaticConfig,
Expand Down
3 changes: 3 additions & 0 deletions pkg/kubernetes/configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ func TestKubernetes_ResolveKubernetesConfigurations_Explicit(t *testing.T) {
}
})
t.Run("with empty file", func(t *testing.T) {
if val := os.Getenv("OPENSHIFT_CI"); val != "" {
t.Skip("this test does not work on OpenShift CI. So we are skipping...")
}
tempDir := t.TempDir()
kubeconfigPath := path.Join(tempDir, "config")
if err := os.WriteFile(kubeconfigPath, []byte(""), 0644); err != nil {
Expand Down
61 changes: 37 additions & 24 deletions pkg/mcp/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,25 +104,38 @@ type mcpContext struct {
listOutput output.Output
logLevel int

staticConfig *config.StaticConfig
clientOptions []transport.ClientOption
before func(*mcpContext)
after func(*mcpContext)
ctx context.Context
tempDir string
cancel context.CancelFunc
mcpServer *Server
mcpHttpServer *httptest.Server
mcpClient *client.Client
klogState klog.State
logBuffer bytes.Buffer
staticConfig *config.StaticConfig
clientOptions []transport.ClientOption
before func(*mcpContext)
after func(*mcpContext)
ctx context.Context
tempDir string
cancel context.CancelFunc
mcpServer *Server
mcpHttpServer *httptest.Server
mcpClient *client.Client
klogState klog.State
logBuffer bytes.Buffer
useEnvTestKubeConfig bool
useInClusterKubeConfig bool
customKubeConfig *rest.Config
}

func (c *mcpContext) beforeEach(t *testing.T) {
var err error
c.ctx, c.cancel = context.WithCancel(t.Context())
c.tempDir = t.TempDir()
c.withKubeConfig(nil)
var kubeConfig string
if c.useEnvTestKubeConfig {
if envTestRestConfig == nil {
panic("shouldn't be empty")
}
_, kubeConfig = c.withKubeConfig(envTestRestConfig)
} else if c.customKubeConfig != nil {
_, kubeConfig = c.withKubeConfig(c.customKubeConfig)
} else if !c.useInClusterKubeConfig {
_, kubeConfig = c.withKubeConfig(nil)
}
if c.profile == nil {
c.profile = &FullProfile{}
}
Expand All @@ -135,6 +148,7 @@ func (c *mcpContext) beforeEach(t *testing.T) {
DisableDestructive: false,
}
}
c.staticConfig.KubeConfig = kubeConfig
if c.before != nil {
c.before(c)
}
Expand Down Expand Up @@ -184,8 +198,13 @@ func (c *mcpContext) afterEach() {
c.klogState.Restore()
}

func testCase(t *testing.T, test func(c *mcpContext)) {
testCaseWithContext(t, &mcpContext{profile: &FullProfile{}}, test)
func testCase(t *testing.T, envTestKubeConfig bool, inClusterKubeConfig bool, customKubeConfig *rest.Config, test func(c *mcpContext)) {
testCaseWithContext(t, &mcpContext{
profile: &FullProfile{},
useEnvTestKubeConfig: envTestKubeConfig,
useInClusterKubeConfig: inClusterKubeConfig,
customKubeConfig: customKubeConfig,
}, test)
}

func testCaseWithContext(t *testing.T, mcpCtx *mcpContext, test func(c *mcpContext)) {
Expand All @@ -195,7 +214,7 @@ func testCaseWithContext(t *testing.T, mcpCtx *mcpContext, test func(c *mcpConte
}

// withKubeConfig sets up a fake kubeconfig in the temp directory based on the provided rest.Config
func (c *mcpContext) withKubeConfig(rc *rest.Config) *api.Config {
func (c *mcpContext) withKubeConfig(rc *rest.Config) (*api.Config, string) {
fakeConfig := api.NewConfig()
fakeConfig.Clusters["fake"] = api.NewCluster()
fakeConfig.Clusters["fake"].Server = "https://127.0.0.1:6443"
Expand All @@ -217,23 +236,17 @@ func (c *mcpContext) withKubeConfig(rc *rest.Config) *api.Config {
fakeConfig.CurrentContext = "fake-context"
kubeConfig := filepath.Join(c.tempDir, "config")
_ = clientcmd.WriteToFile(*fakeConfig, kubeConfig)
_ = os.Setenv("KUBECONFIG", kubeConfig)
if c.mcpServer != nil {
c.mcpServer.configuration.StaticConfig.KubeConfig = kubeConfig
if err := c.mcpServer.reloadKubernetesClient(); err != nil {
panic(err)
}
}
return fakeConfig
}

// withEnvTest sets up the environment for kubeconfig to be used with envTest
func (c *mcpContext) withEnvTest() {
c.withKubeConfig(envTestRestConfig)
return fakeConfig, kubeConfig
}

// inOpenShift sets up the kubernetes environment to seem to be running OpenShift
func inOpenShift(c *mcpContext) {
c.withEnvTest()
crdTemplate := `
{
"apiVersion": "apiextensions.k8s.io/v1",
Expand Down
4 changes: 2 additions & 2 deletions pkg/mcp/configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
)

func TestConfigurationView(t *testing.T) {
testCase(t, func(c *mcpContext) {
testCase(t, false, false, nil, func(c *mcpContext) {
toolResult, err := c.callTool("configuration_view", map[string]interface{}{})
t.Run("configuration_view returns configuration", func(t *testing.T) {
if err != nil {
Expand Down Expand Up @@ -122,7 +122,7 @@ func TestConfigurationViewInCluster(t *testing.T) {
defer func() {
kubernetes.InClusterConfig = rest.InClusterConfig
}()
testCase(t, func(c *mcpContext) {
testCase(t, false, true, nil, func(c *mcpContext) {
toolResult, err := c.callTool("configuration_view", map[string]interface{}{})
t.Run("configuration_view returns configuration", func(t *testing.T) {
if err != nil {
Expand Down
6 changes: 2 additions & 4 deletions pkg/mcp/events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import (
)

func TestEventsList(t *testing.T) {
testCase(t, func(c *mcpContext) {
c.withEnvTest()
testCase(t, true, false, nil, func(c *mcpContext) {
toolResult, err := c.callTool("events_list", map[string]interface{}{})
t.Run("events_list with no events returns OK", func(t *testing.T) {
if err != nil {
Expand Down Expand Up @@ -97,8 +96,7 @@ 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()
testCaseWithContext(t, &mcpContext{staticConfig: deniedResourcesServer, useEnvTestKubeConfig: true}, func(c *mcpContext) {
eventList, _ := c.callTool("events_list", map[string]interface{}{})
t.Run("events_list has error", func(t *testing.T) {
if !eventList.IsError {
Expand Down
67 changes: 43 additions & 24 deletions pkg/mcp/helm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,40 @@ package mcp
import (
"context"
"encoding/base64"
"path/filepath"
"runtime"
"strings"
"testing"

"github.com/containers/kubernetes-mcp-server/pkg/config"
"github.com/mark3labs/mcp-go/mcp"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"path/filepath"
"runtime"
"k8s.io/klog/v2"
"sigs.k8s.io/yaml"
"strings"
"testing"
)

func TestHelmInstall(t *testing.T) {
testCase(t, func(c *mcpContext) {
c.withEnvTest()
_, file, _, _ := runtime.Caller(0)
testCase(t, true, false, nil, func(c *mcpContext) {
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatalf("could not get caller info")
}
ns := c.mcpServer.k.NamespaceOrDefault("default")
klog.Infof("namespace: %s will be used for helm chart installation", ns)
chartPath := filepath.Join(filepath.Dir(file), "testdata", "helm-chart-no-op")
toolResult, err := c.callTool("helm_install", map[string]interface{}{
"chart": chartPath,
"chart": chartPath,
"namespace": ns,
})
t.Run("helm_install with local chart and no release name, returns installed chart", func(t *testing.T) {
if err != nil {
t.Fatalf("call tool failed %v", err)
}
if toolResult.IsError {
t.Fatalf("call tool failed")
t.Fatalf("call tool failed %s", toolResult.Content)
}
var decoded []map[string]interface{}
err = yaml.Unmarshal([]byte(toolResult.Content[0].(mcp.TextContent).Text), &decoded)
Expand Down Expand Up @@ -60,12 +67,16 @@ func TestHelmInstall(t *testing.T) {

func TestHelmInstallDenied(t *testing.T) {
deniedResourcesServer := &config.StaticConfig{DeniedResources: []config.GroupVersionKind{{Version: "v1", Kind: "Secret"}}}
testCaseWithContext(t, &mcpContext{staticConfig: deniedResourcesServer}, func(c *mcpContext) {
c.withEnvTest()
testCaseWithContext(t, &mcpContext{staticConfig: deniedResourcesServer, useEnvTestKubeConfig: true}, func(c *mcpContext) {
_, file, _, _ := runtime.Caller(0)
chartPath := filepath.Join(filepath.Dir(file), "testdata", "helm-chart-secret")

ns := c.mcpServer.k.NamespaceOrDefault("default")
klog.Infof("namespace: %s will be used for helm chart installation", ns)

helmInstall, _ := c.callTool("helm_install", map[string]interface{}{
"chart": chartPath,
"chart": chartPath,
"namespace": ns,
})
t.Run("helm_install has error", func(t *testing.T) {
if !helmInstall.IsError {
Expand All @@ -83,11 +94,16 @@ func TestHelmInstallDenied(t *testing.T) {
}

func TestHelmList(t *testing.T) {
testCase(t, func(c *mcpContext) {
c.withEnvTest()
testCase(t, true, false, nil, func(c *mcpContext) {
kc := c.newKubernetesClient()
clearHelmReleases(c.ctx, kc)
toolResult, err := c.callTool("helm_list", map[string]interface{}{})

ns := c.mcpServer.k.NamespaceOrDefault("default")
klog.Infof("namespace: %s will be used for helm chart installation", ns)

toolResult, err := c.callTool("helm_list", map[string]interface{}{
"namespace": ns,
})
t.Run("helm_list with no releases, returns not found", func(t *testing.T) {
if err != nil {
t.Fatalf("call tool failed %v", err)
Expand All @@ -99,7 +115,7 @@ func TestHelmList(t *testing.T) {
t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
}
})
_, _ = kc.CoreV1().Secrets("default").Create(c.ctx, &corev1.Secret{
_, _ = kc.CoreV1().Secrets(ns).Create(c.ctx, &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "sh.helm.release.v1.release-to-list",
Labels: map[string]string{"owner": "helm", "name": "release-to-list"},
Expand All @@ -111,7 +127,9 @@ func TestHelmList(t *testing.T) {
"}"))),
},
}, metav1.CreateOptions{})
toolResult, err = c.callTool("helm_list", map[string]interface{}{})
toolResult, err = c.callTool("helm_list", map[string]interface{}{
"namespace": ns,
})
t.Run("helm_list with deployed release, returns release", func(t *testing.T) {
if err != nil {
t.Fatalf("call tool failed %v", err)
Expand Down Expand Up @@ -173,12 +191,12 @@ func TestHelmList(t *testing.T) {
}

func TestHelmUninstall(t *testing.T) {
testCase(t, func(c *mcpContext) {
c.withEnvTest()
testCase(t, true, false, nil, func(c *mcpContext) {
kc := c.newKubernetesClient()
clearHelmReleases(c.ctx, kc)
toolResult, err := c.callTool("helm_uninstall", map[string]interface{}{
"name": "release-to-uninstall",
"name": "release-to-uninstall",
"namespace": "default",
})
t.Run("helm_uninstall with no releases, returns not found", func(t *testing.T) {
if err != nil {
Expand All @@ -204,7 +222,8 @@ func TestHelmUninstall(t *testing.T) {
},
}, metav1.CreateOptions{})
toolResult, err = c.callTool("helm_uninstall", map[string]interface{}{
"name": "existent-release-to-uninstall",
"name": "existent-release-to-uninstall",
"namespace": "default",
})
t.Run("helm_uninstall with deployed release, returns uninstalled", func(t *testing.T) {
if err != nil {
Expand All @@ -226,8 +245,7 @@ func TestHelmUninstall(t *testing.T) {

func TestHelmUninstallDenied(t *testing.T) {
deniedResourcesServer := &config.StaticConfig{DeniedResources: []config.GroupVersionKind{{Version: "v1", Kind: "Secret"}}}
testCaseWithContext(t, &mcpContext{staticConfig: deniedResourcesServer}, func(c *mcpContext) {
c.withEnvTest()
testCaseWithContext(t, &mcpContext{staticConfig: deniedResourcesServer, useEnvTestKubeConfig: true}, func(c *mcpContext) {
kc := c.newKubernetesClient()
clearHelmReleases(c.ctx, kc)
_, _ = kc.CoreV1().Secrets("default").Create(c.ctx, &corev1.Secret{
Expand All @@ -244,7 +262,8 @@ func TestHelmUninstallDenied(t *testing.T) {
},
}, metav1.CreateOptions{})
helmUninstall, _ := c.callTool("helm_uninstall", map[string]interface{}{
"name": "existent-release-to-uninstall",
"name": "existent-release-to-uninstall",
"namespace": "default",
})
t.Run("helm_uninstall has error", func(t *testing.T) {
if !helmUninstall.IsError {
Expand Down
2 changes: 1 addition & 1 deletion pkg/mcp/mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func TestWatchKubeConfig(t *testing.T) {
if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
t.Skip("Skipping test on non-Unix-like platforms")
}
testCase(t, func(c *mcpContext) {
testCase(t, false, false, nil, func(c *mcpContext) {
// Given
withTimeout, cancel := context.WithTimeout(c.ctx, 5*time.Second)
defer cancel()
Expand Down
23 changes: 17 additions & 6 deletions pkg/mcp/mcp_tools_test.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
package mcp

import (
"github.com/mark3labs/mcp-go/client/transport"
"github.com/mark3labs/mcp-go/mcp"
"k8s.io/utils/ptr"
"regexp"
"strings"
"testing"

"github.com/mark3labs/mcp-go/client/transport"
"github.com/mark3labs/mcp-go/mcp"
"k8s.io/utils/ptr"

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

func TestUnrestricted(t *testing.T) {
testCase(t, func(c *mcpContext) {
testCase(t, false, false, nil, func(c *mcpContext) {
tools, err := c.mcpClient.ListTools(c.ctx, mcp.ListToolsRequest{})
t.Run("ListTools returns tools", func(t *testing.T) {
if err != nil {
Expand All @@ -32,7 +33,12 @@ func TestUnrestricted(t *testing.T) {
}

func TestReadOnly(t *testing.T) {
readOnlyServer := func(c *mcpContext) { c.staticConfig = &config.StaticConfig{ReadOnly: true} }
readOnlyServer := func(c *mcpContext) {
c.staticConfig = &config.StaticConfig{
ReadOnly: true,
KubeConfig: c.staticConfig.KubeConfig,
}
}
testCaseWithContext(t, &mcpContext{before: readOnlyServer}, func(c *mcpContext) {
tools, err := c.mcpClient.ListTools(c.ctx, mcp.ListToolsRequest{})
t.Run("ListTools returns tools", func(t *testing.T) {
Expand All @@ -54,7 +60,12 @@ func TestReadOnly(t *testing.T) {
}

func TestDisableDestructive(t *testing.T) {
disableDestructiveServer := func(c *mcpContext) { c.staticConfig = &config.StaticConfig{DisableDestructive: true} }
disableDestructiveServer := func(c *mcpContext) {
c.staticConfig = &config.StaticConfig{
DisableDestructive: true,
KubeConfig: c.staticConfig.KubeConfig,
}
}
testCaseWithContext(t, &mcpContext{before: disableDestructiveServer}, func(c *mcpContext) {
tools, err := c.mcpClient.ListTools(c.ctx, mcp.ListToolsRequest{})
t.Run("ListTools returns tools", func(t *testing.T) {
Expand Down
Loading
Loading