feat: configuration view works in cluster

This commit is contained in:
Marc Nuri
2025-03-21 14:57:41 +01:00
parent 094da788e7
commit 32b388aab3
4 changed files with 91 additions and 28 deletions

View File

@@ -6,9 +6,24 @@ import (
)
func ConfigurationView() (string, error) {
// TODO: consider in cluster run mode (current approach only shows kubeconfig)
cfg, err := resolveConfig().RawConfig()
if err != nil {
var cfg clientcmdapi.Config
var err error
inClusterConfig, err := InClusterConfig()
if err == nil && inClusterConfig != nil {
cfg = *clientcmdapi.NewConfig()
cfg.Clusters["cluster"] = &clientcmdapi.Cluster{
Server: inClusterConfig.Host,
InsecureSkipTLSVerify: inClusterConfig.Insecure,
}
cfg.AuthInfos["user"] = &clientcmdapi.AuthInfo{
Token: inClusterConfig.BearerToken,
}
cfg.Contexts["context"] = &clientcmdapi.Context{
Cluster: "cluster",
AuthInfo: "user",
}
cfg.CurrentContext = "context"
} else if cfg, err = resolveConfig().RawConfig(); err != nil {
return "", err
}
if err = clientcmdapi.MinifyConfig(&cfg); err != nil {

View File

@@ -13,6 +13,10 @@ import (
"sigs.k8s.io/yaml"
)
// InClusterConfig is a variable that holds the function to get the in-cluster config
// Exposed for testing
var InClusterConfig = rest.InClusterConfig
type Kubernetes struct {
cfg *rest.Config
clientSet *kubernetes.Clientset
@@ -71,24 +75,13 @@ func marshal(v any) (string, error) {
func resolveConfig() clientcmd.ClientConfig {
pathOptions := clientcmd.NewDefaultPathOptions()
//cfg, err := pathOptions.GetStartingConfig()
//if err != nil {
// return nil, err
//}
//if err = clientcmdapi.MinifyConfig(cfg); err != nil {
// return nil, err
//}
//if err = clientcmdapi.FlattenConfig(cfg); err != nil {
// return nil, err
//}
//return cfg, nil
return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
&clientcmd.ClientConfigLoadingRules{ExplicitPath: pathOptions.GetDefaultFilename()},
&clientcmd.ConfigOverrides{ClusterInfo: clientcmdapi.Cluster{Server: ""}})
}
func resolveClientConfig() (*rest.Config, error) {
inClusterConfig, err := rest.InClusterConfig()
inClusterConfig, err := InClusterConfig()
if err == nil && inClusterConfig != nil {
return inClusterConfig, nil
}

View File

@@ -19,7 +19,7 @@ func (s *Server) initConfiguration() []server.ServerTool {
func configurationView(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
ret, err := kubernetes.ConfigurationView()
if err != nil {
err = fmt.Errorf("failed to get configuration view: %v", err)
err = fmt.Errorf("failed to get configuration: %v", err)
}
return NewTextResult(ret, err), nil
}

View File

@@ -1,7 +1,9 @@
package mcp
import (
"github.com/manusa/kubernetes-mcp-server/pkg/kubernetes"
"github.com/mark3labs/mcp-go/mcp"
"k8s.io/client-go/rest"
v1 "k8s.io/client-go/tools/clientcmd/api/v1"
"sigs.k8s.io/yaml"
"testing"
@@ -13,7 +15,6 @@ func TestConfigurationView(t *testing.T) {
t.Run("configuration_view returns configuration", func(t *testing.T) {
if err != nil {
t.Fatalf("call tool failed %v", err)
return
}
})
var decoded *v1.Config
@@ -21,55 +22,109 @@ func TestConfigurationView(t *testing.T) {
t.Run("configuration_view has yaml content", func(t *testing.T) {
if err != nil {
t.Fatalf("invalid tool result content %v", err)
return
}
})
t.Run("configuration_view returns current-context", func(t *testing.T) {
if decoded.CurrentContext != "fake-context" {
t.Fatalf("fake-context not found: %v", decoded.CurrentContext)
return
}
})
t.Run("configuration_view returns context info", func(t *testing.T) {
if len(decoded.Contexts) != 1 {
t.Fatalf("invalid context count, expected 1, got %v", len(decoded.Contexts))
return
}
if decoded.Contexts[0].Name != "fake-context" {
t.Fatalf("fake-context not found: %v", decoded.Contexts)
return
}
if decoded.Contexts[0].Context.Cluster != "fake" {
t.Fatalf("fake-cluster not found: %v", decoded.Contexts)
return
}
if decoded.Contexts[0].Context.AuthInfo != "fake" {
t.Fatalf("fake-auth not found: %v", decoded.Contexts)
return
}
})
t.Run("configuration_view returns cluster info", func(t *testing.T) {
if len(decoded.Clusters) != 1 {
t.Fatalf("invalid cluster count, expected 1, got %v", len(decoded.Clusters))
return
}
if decoded.Clusters[0].Name != "fake" {
t.Fatalf("fake-cluster not found: %v", decoded.Clusters)
return
}
if decoded.Clusters[0].Cluster.Server != "https://example.com" {
t.Fatalf("fake-server not found: %v", decoded.Clusters)
return
}
})
t.Run("configuration_view returns auth info", func(t *testing.T) {
if len(decoded.AuthInfos) != 1 {
t.Fatalf("invalid auth info count, expected 1, got %v", len(decoded.AuthInfos))
return
}
if decoded.AuthInfos[0].Name != "fake" {
t.Fatalf("fake-auth not found: %v", decoded.AuthInfos)
return
}
})
})
}
func TestConfigurationViewInCluster(t *testing.T) {
kubernetes.InClusterConfig = func() (*rest.Config, error) {
return &rest.Config{
Host: "https://kubernetes.default.svc",
BearerToken: "fake-token",
}, nil
}
defer func() {
kubernetes.InClusterConfig = rest.InClusterConfig
}()
testCase(t, 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 {
t.Fatalf("call tool failed %v", err)
}
})
var decoded *v1.Config
err = yaml.Unmarshal([]byte(toolResult.Content[0].(mcp.TextContent).Text), &decoded)
t.Run("configuration_view has yaml content", func(t *testing.T) {
if err != nil {
t.Fatalf("invalid tool result content %v", err)
}
})
t.Run("configuration_view returns current-context", func(t *testing.T) {
if decoded.CurrentContext != "context" {
t.Fatalf("context not found: %v", decoded.CurrentContext)
}
})
t.Run("configuration_view returns context info", func(t *testing.T) {
if len(decoded.Contexts) != 1 {
t.Fatalf("invalid context count, expected 1, got %v", len(decoded.Contexts))
}
if decoded.Contexts[0].Name != "context" {
t.Fatalf("context not found: %v", decoded.Contexts)
}
if decoded.Contexts[0].Context.Cluster != "cluster" {
t.Fatalf("cluster not found: %v", decoded.Contexts)
}
if decoded.Contexts[0].Context.AuthInfo != "user" {
t.Fatalf("user not found: %v", decoded.Contexts)
}
})
t.Run("configuration_view returns cluster info", func(t *testing.T) {
if len(decoded.Clusters) != 1 {
t.Fatalf("invalid cluster count, expected 1, got %v", len(decoded.Clusters))
}
if decoded.Clusters[0].Name != "cluster" {
t.Fatalf("cluster not found: %v", decoded.Clusters)
}
if decoded.Clusters[0].Cluster.Server != "https://kubernetes.default.svc" {
t.Fatalf("server not found: %v", decoded.Clusters)
}
})
t.Run("configuration_view returns auth info", func(t *testing.T) {
if len(decoded.AuthInfos) != 1 {
t.Fatalf("invalid auth info count, expected 1, got %v", len(decoded.AuthInfos))
}
if decoded.AuthInfos[0].Name != "user" {
t.Fatalf("user not found: %v", decoded.AuthInfos)
}
})
})