mirror of
https://github.com/containers/kubernetes-mcp-server.git
synced 2025-10-23 01:22:57 +03:00
refactor(helm): adapt Helm contribution to project structure
This commit is contained in:
@@ -1,50 +1,52 @@
|
||||
package helm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
"helm.sh/helm/v3/pkg/cli"
|
||||
"helm.sh/helm/v3/pkg/release"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"log"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// Helm provides methods to interact with Helm releases
|
||||
// Mirrors the abstraction style of pkg/kubernetes
|
||||
|
||||
type Helm struct {
|
||||
settings *cli.EnvSettings
|
||||
type Kubernetes interface {
|
||||
genericclioptions.RESTClientGetter
|
||||
NamespaceOrDefault(namespace string) string
|
||||
}
|
||||
|
||||
// NewHelm creates a new Helm instance using kubeconfig, context, and namespace settings
|
||||
func NewHelm(kubeconfig, kubeContext, namespace string) *Helm {
|
||||
type Helm struct {
|
||||
kubernetes Kubernetes
|
||||
}
|
||||
|
||||
// NewHelm creates a new Helm instance
|
||||
func NewHelm(kubernetes Kubernetes, namespace string) *Helm {
|
||||
settings := cli.New()
|
||||
if kubeconfig != "" {
|
||||
settings.KubeConfig = kubeconfig
|
||||
}
|
||||
if kubeContext != "" {
|
||||
settings.KubeContext = kubeContext
|
||||
}
|
||||
if namespace != "" {
|
||||
settings.SetNamespace(namespace)
|
||||
}
|
||||
return &Helm{settings: settings}
|
||||
return &Helm{kubernetes: kubernetes}
|
||||
}
|
||||
|
||||
// ReleasesList lists Helm releases in a specific namespace (or all namespaces if namespace is empty)
|
||||
func (h *Helm) ReleasesList(ctx context.Context, namespace string) ([]*release.Release, error) {
|
||||
// If no namespace is given, use the default from kubeconfig
|
||||
if namespace == "" {
|
||||
namespace = h.settings.Namespace()
|
||||
}
|
||||
// ReleasesList lists all the releases for the specified namespace (or current namespace if). Or allNamespaces is true, it lists all releases across all namespaces.
|
||||
func (h *Helm) ReleasesList(namespace string, allNamespaces bool) (string, error) {
|
||||
cfg := new(action.Configuration)
|
||||
if err := cfg.Init(h.settings.RESTClientGetter(), namespace, "", log.Printf); err != nil {
|
||||
return nil, err
|
||||
applicableNamespace := ""
|
||||
if !allNamespaces {
|
||||
applicableNamespace = h.kubernetes.NamespaceOrDefault(namespace)
|
||||
}
|
||||
if err := cfg.Init(h.kubernetes, applicableNamespace, "", log.Printf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
list := action.NewList(cfg)
|
||||
// To list across all namespaces, set AllNamespaces to true
|
||||
if namespace == "" || namespace == "all" {
|
||||
list.AllNamespaces = true
|
||||
list.AllNamespaces = allNamespaces
|
||||
releases, err := list.Run()
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if len(releases) == 0 {
|
||||
return "No Helm releases found", nil
|
||||
}
|
||||
return list.Run()
|
||||
ret, err := yaml.Marshal(releases)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(ret), nil
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
package helm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewHelm(t *testing.T) {
|
||||
h := NewHelm("", "", "")
|
||||
if h == nil {
|
||||
t.Fatal("expected non-nil Helm instance")
|
||||
}
|
||||
if h.settings == nil {
|
||||
t.Fatal("expected non-nil settings in Helm instance")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelm_ReleasesList_DefaultNamespace(t *testing.T) {
|
||||
h := NewHelm("", "", "")
|
||||
// Use a namespace that is likely to exist, or leave empty for default
|
||||
releases, err := h.ReleasesList(context.Background(), "")
|
||||
if err != nil {
|
||||
t.Skipf("skipping: could not list releases (likely no cluster/helm configured): %v", err)
|
||||
}
|
||||
// No assertion on releases count, just check type
|
||||
if releases == nil {
|
||||
t.Error("expected releases slice, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelm_ReleasesList_AllNamespaces(t *testing.T) {
|
||||
h := NewHelm("", "", "")
|
||||
releases, err := h.ReleasesList(context.Background(), "all")
|
||||
if err != nil {
|
||||
t.Skipf("skipping: could not list all releases (likely no cluster/helm configured): %v", err)
|
||||
}
|
||||
if releases == nil {
|
||||
t.Error("expected releases slice, got nil")
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,30 @@ func (k *Kubernetes) IsInCluster() bool {
|
||||
return err == nil && cfg != nil
|
||||
}
|
||||
|
||||
func (k *Kubernetes) configuredNamespace() string {
|
||||
if ns, _, nsErr := k.clientCmdConfig.Namespace(); nsErr == nil {
|
||||
return ns
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (k *Kubernetes) NamespaceOrDefault(namespace string) string {
|
||||
if namespace == "" {
|
||||
return k.configuredNamespace()
|
||||
}
|
||||
return namespace
|
||||
}
|
||||
|
||||
// ToRESTConfig returns the rest.Config object (genericclioptions.RESTClientGetter)
|
||||
func (k *Kubernetes) ToRESTConfig() (*rest.Config, error) {
|
||||
return k.cfg, nil
|
||||
}
|
||||
|
||||
// ToRawKubeConfigLoader returns the clientcmd.ClientConfig object (genericclioptions.RESTClientGetter)
|
||||
func (k *Kubernetes) ToRawKubeConfigLoader() clientcmd.ClientConfig {
|
||||
return k.clientCmdConfig
|
||||
}
|
||||
|
||||
func (k *Kubernetes) ConfigurationView(minify bool) (string, error) {
|
||||
var cfg clientcmdapi.Config
|
||||
var err error
|
||||
|
||||
@@ -2,7 +2,9 @@ package kubernetes
|
||||
|
||||
import (
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/manusa/kubernetes-mcp-server/pkg/helm"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/discovery"
|
||||
@@ -26,9 +28,10 @@ type Kubernetes struct {
|
||||
scheme *runtime.Scheme
|
||||
parameterCodec runtime.ParameterCodec
|
||||
clientSet kubernetes.Interface
|
||||
discoveryClient *discovery.DiscoveryClient
|
||||
discoveryClient discovery.CachedDiscoveryInterface
|
||||
deferredDiscoveryRESTMapper *restmapper.DeferredDiscoveryRESTMapper
|
||||
dynamicClient *dynamic.DynamicClient
|
||||
Helm *helm.Helm
|
||||
}
|
||||
|
||||
func NewKubernetes(kubeconfig string) (*Kubernetes, error) {
|
||||
@@ -43,10 +46,11 @@ func NewKubernetes(kubeconfig string) (*Kubernetes, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
k8s.discoveryClient, err = discovery.NewDiscoveryClientForConfig(k8s.cfg)
|
||||
discoveryClient, err := discovery.NewDiscoveryClientForConfig(k8s.cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
k8s.discoveryClient = memory.NewMemCacheClient(discoveryClient)
|
||||
k8s.deferredDiscoveryRESTMapper = restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(k8s.discoveryClient))
|
||||
k8s.dynamicClient, err = dynamic.NewForConfig(k8s.cfg)
|
||||
if err != nil {
|
||||
@@ -57,6 +61,7 @@ func NewKubernetes(kubeconfig string) (*Kubernetes, error) {
|
||||
return nil, err
|
||||
}
|
||||
k8s.parameterCodec = runtime.NewParameterCodec(k8s.scheme)
|
||||
k8s.Helm = helm.NewHelm(k8s, "TODO")
|
||||
return k8s, nil
|
||||
}
|
||||
|
||||
@@ -102,6 +107,14 @@ func (k *Kubernetes) Close() {
|
||||
}
|
||||
}
|
||||
|
||||
func (k *Kubernetes) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
|
||||
return k.discoveryClient, nil
|
||||
}
|
||||
|
||||
func (k *Kubernetes) ToRESTMapper() (meta.RESTMapper, error) {
|
||||
return k.deferredDiscoveryRESTMapper, nil
|
||||
}
|
||||
|
||||
func marshal(v any) (string, error) {
|
||||
switch t := v.(type) {
|
||||
case []unstructured.Unstructured:
|
||||
@@ -123,37 +136,3 @@ func marshal(v any) (string, error) {
|
||||
}
|
||||
return string(ret), nil
|
||||
}
|
||||
|
||||
// KubeconfigPath returns the kubeconfig path used by this Kubernetes client
|
||||
func (k *Kubernetes) KubeconfigPath() string {
|
||||
return k.Kubeconfig
|
||||
}
|
||||
|
||||
// CurrentContext returns the current context from the kubeconfig
|
||||
func (k *Kubernetes) CurrentContext() string {
|
||||
if k.clientCmdConfig == nil {
|
||||
return ""
|
||||
}
|
||||
if rawConfig, err := k.clientCmdConfig.RawConfig(); err == nil {
|
||||
return rawConfig.CurrentContext
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ConfiguredNamespace returns the namespace configured in the kubeconfig/context
|
||||
func (k *Kubernetes) ConfiguredNamespace() string {
|
||||
if k.clientCmdConfig == nil {
|
||||
return ""
|
||||
}
|
||||
if ns, _, nsErr := k.clientCmdConfig.Namespace(); nsErr == nil {
|
||||
return ns
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (k *Kubernetes) namespaceOrDefault(namespace string) string {
|
||||
if namespace == "" {
|
||||
return k.ConfiguredNamespace()
|
||||
}
|
||||
return namespace
|
||||
}
|
||||
|
||||
@@ -32,11 +32,11 @@ func (k *Kubernetes) PodsListInNamespace(ctx context.Context, namespace string)
|
||||
func (k *Kubernetes) PodsGet(ctx context.Context, namespace, name string) (string, error) {
|
||||
return k.ResourcesGet(ctx, &schema.GroupVersionKind{
|
||||
Group: "", Version: "v1", Kind: "Pod",
|
||||
}, k.namespaceOrDefault(namespace), name)
|
||||
}, k.NamespaceOrDefault(namespace), name)
|
||||
}
|
||||
|
||||
func (k *Kubernetes) PodsDelete(ctx context.Context, namespace, name string) (string, error) {
|
||||
namespace = k.namespaceOrDefault(namespace)
|
||||
namespace = k.NamespaceOrDefault(namespace)
|
||||
pod, err := k.clientSet.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -79,7 +79,7 @@ func (k *Kubernetes) PodsDelete(ctx context.Context, namespace, name string) (st
|
||||
|
||||
func (k *Kubernetes) PodsLog(ctx context.Context, namespace, name, container string) (string, error) {
|
||||
tailLines := int64(256)
|
||||
req := k.clientSet.CoreV1().Pods(k.namespaceOrDefault(namespace)).GetLogs(name, &v1.PodLogOptions{
|
||||
req := k.clientSet.CoreV1().Pods(k.NamespaceOrDefault(namespace)).GetLogs(name, &v1.PodLogOptions{
|
||||
TailLines: &tailLines,
|
||||
Container: container,
|
||||
})
|
||||
@@ -108,7 +108,7 @@ func (k *Kubernetes) PodsRun(ctx context.Context, namespace, name, image string,
|
||||
var resources []any
|
||||
pod := &v1.Pod{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: k.namespaceOrDefault(namespace), Labels: labels},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: k.NamespaceOrDefault(namespace), Labels: labels},
|
||||
Spec: v1.PodSpec{Containers: []v1.Container{{
|
||||
Name: name,
|
||||
Image: image,
|
||||
@@ -120,7 +120,7 @@ func (k *Kubernetes) PodsRun(ctx context.Context, namespace, name, image string,
|
||||
pod.Spec.Containers[0].Ports = []v1.ContainerPort{{ContainerPort: port}}
|
||||
resources = append(resources, &v1.Service{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Service"},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: k.namespaceOrDefault(namespace), Labels: labels},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: k.NamespaceOrDefault(namespace), Labels: labels},
|
||||
Spec: v1.ServiceSpec{
|
||||
Selector: labels,
|
||||
Type: v1.ServiceTypeClusterIP,
|
||||
@@ -135,7 +135,7 @@ func (k *Kubernetes) PodsRun(ctx context.Context, namespace, name, image string,
|
||||
"kind": "Route",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": name,
|
||||
"namespace": k.namespaceOrDefault(namespace),
|
||||
"namespace": k.NamespaceOrDefault(namespace),
|
||||
"labels": labels,
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
@@ -175,7 +175,7 @@ func (k *Kubernetes) PodsRun(ctx context.Context, namespace, name, image string,
|
||||
}
|
||||
|
||||
func (k *Kubernetes) PodsExec(ctx context.Context, namespace, name, container string, command []string) (string, error) {
|
||||
namespace = k.namespaceOrDefault(namespace)
|
||||
namespace = k.NamespaceOrDefault(namespace)
|
||||
pod, err := k.clientSet.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -34,7 +34,7 @@ func (k *Kubernetes) ResourcesGet(ctx context.Context, gvk *schema.GroupVersionK
|
||||
}
|
||||
// If it's a namespaced resource and namespace wasn't provided, try to use the default configured one
|
||||
if namespaced, nsErr := k.isNamespaced(gvk); nsErr == nil && namespaced {
|
||||
namespace = k.namespaceOrDefault(namespace)
|
||||
namespace = k.NamespaceOrDefault(namespace)
|
||||
}
|
||||
rg, err := k.dynamicClient.Resource(*gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
@@ -64,7 +64,7 @@ func (k *Kubernetes) ResourcesDelete(ctx context.Context, gvk *schema.GroupVersi
|
||||
}
|
||||
// If it's a namespaced resource and namespace wasn't provided, try to use the default configured one
|
||||
if namespaced, nsErr := k.isNamespaced(gvk); nsErr == nil && namespaced {
|
||||
namespace = k.namespaceOrDefault(namespace)
|
||||
namespace = k.NamespaceOrDefault(namespace)
|
||||
}
|
||||
return k.dynamicClient.Resource(*gvr).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{})
|
||||
}
|
||||
@@ -77,7 +77,7 @@ func (k *Kubernetes) resourcesList(ctx context.Context, gvk *schema.GroupVersion
|
||||
// Check if operation is allowed for all namespaces (applicable for namespaced resources)
|
||||
isNamespaced, _ := k.isNamespaced(gvk)
|
||||
if isNamespaced && !k.canIUse(ctx, gvr, namespace, "list") && namespace == "" {
|
||||
namespace = k.ConfiguredNamespace()
|
||||
namespace = k.configuredNamespace()
|
||||
}
|
||||
return k.dynamicClient.Resource(*gvr).Namespace(namespace).List(ctx, metav1.ListOptions{})
|
||||
}
|
||||
@@ -92,7 +92,7 @@ func (k *Kubernetes) resourcesCreateOrUpdate(ctx context.Context, resources []*u
|
||||
namespace := obj.GetNamespace()
|
||||
// If it's a namespaced resource and namespace wasn't provided, try to use the default configured one
|
||||
if namespaced, nsErr := k.isNamespaced(&gvk); nsErr == nil && namespaced {
|
||||
namespace = k.namespaceOrDefault(namespace)
|
||||
namespace = k.NamespaceOrDefault(namespace)
|
||||
}
|
||||
resources[i], rErr = k.dynamicClient.Resource(*gvr).Namespace(namespace).Apply(ctx, obj.GetName(), obj, metav1.ApplyOptions{
|
||||
FieldManager: version.BinaryName,
|
||||
|
||||
@@ -5,64 +5,33 @@ import (
|
||||
"fmt"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func (s *Server) initHelm() []server.ServerTool {
|
||||
rets := make([]server.ServerTool, 0)
|
||||
rets = append(rets, server.ServerTool{
|
||||
Tool: mcp.NewTool("helm_list",
|
||||
mcp.WithDescription("List all Helm releases in all namespaces."),
|
||||
mcp.WithDescription("List all of the Helm releases in the current or provided namespace (or in all namespaces if specified)"),
|
||||
mcp.WithString("namespace", mcp.Description("Namespace to list Helm releases from (Optional, all namespaces if not provided)")),
|
||||
mcp.WithBoolean("all_namespaces", mcp.Description("If true, lists all Helm releases in all namespaces ignoring the namespace argument (Optional)")),
|
||||
),
|
||||
Handler: s.helmReleasesList,
|
||||
})
|
||||
rets = append(rets, server.ServerTool{
|
||||
Tool: mcp.NewTool("helm_list_in_namespace",
|
||||
mcp.WithDescription("List all Helm releases in the specified namespace."),
|
||||
mcp.WithString("namespace", mcp.Description("Namespace to list Helm releases from."), mcp.Required()),
|
||||
),
|
||||
Handler: s.helmListInNamespace,
|
||||
Handler: s.helmList,
|
||||
})
|
||||
return rets
|
||||
}
|
||||
|
||||
func (s *Server) helmReleasesList(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
releases, err := s.helm.ReleasesList(ctx, "")
|
||||
func (s *Server) helmList(_ context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
allNamespaces := false
|
||||
if v, ok := ctr.Params.Arguments["all_namespaces"].(bool); ok {
|
||||
allNamespaces = v
|
||||
}
|
||||
namespace := ""
|
||||
if v, ok := ctr.Params.Arguments["namespace"].(string); ok {
|
||||
namespace = v
|
||||
}
|
||||
ret, err := s.k.Helm.ReleasesList(namespace, allNamespaces)
|
||||
if err != nil {
|
||||
return NewTextResult("", fmt.Errorf("failed to list helm releases: %w", err)), nil
|
||||
return NewTextResult("", fmt.Errorf("failed to list helm releases in namespace '%s': %w", namespace, err)), nil
|
||||
}
|
||||
for _, r := range releases {
|
||||
if r != nil && r.Chart != nil {
|
||||
r.Chart.Templates = nil
|
||||
r.Chart.Files = nil
|
||||
}
|
||||
}
|
||||
out, err := yaml.Marshal(releases)
|
||||
if err != nil {
|
||||
return NewTextResult("", fmt.Errorf("failed to marshal helm releases: %w", err)), nil
|
||||
}
|
||||
return NewTextResult(string(out), nil), nil
|
||||
}
|
||||
|
||||
// helmListInNamespace lists Helm releases in a specified namespace
|
||||
func (s *Server) helmListInNamespace(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
ns := req.Params.Arguments["namespace"]
|
||||
if ns == nil || ns.(string) == "" {
|
||||
return NewTextResult("", fmt.Errorf("missing required argument: namespace")), nil
|
||||
}
|
||||
releases, err := s.helm.ReleasesList(ctx, ns.(string))
|
||||
if err != nil {
|
||||
return NewTextResult("", fmt.Errorf("failed to list helm releases in namespace %s: %w", ns, err)), nil
|
||||
}
|
||||
for _, r := range releases {
|
||||
if r != nil && r.Chart != nil {
|
||||
r.Chart.Templates = nil
|
||||
r.Chart.Files = nil
|
||||
}
|
||||
}
|
||||
out, err := yaml.Marshal(releases)
|
||||
if err != nil {
|
||||
return NewTextResult("", fmt.Errorf("failed to marshal helm releases: %w", err)), nil
|
||||
}
|
||||
return NewTextResult(string(out), nil), nil
|
||||
return NewTextResult(ret, err), nil
|
||||
}
|
||||
|
||||
100
pkg/mcp/helm_test.go
Normal file
100
pkg/mcp/helm_test.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHelmList(t *testing.T) {
|
||||
testCase(t, func(c *mcpContext) {
|
||||
c.withEnvTest()
|
||||
kc := c.newKubernetesClient()
|
||||
_ = kc.CoreV1().Secrets("default").Delete(c.ctx, "release-to-list", metav1.DeleteOptions{})
|
||||
toolResult, err := c.callTool("helm_list", map[string]interface{}{})
|
||||
t.Run("helm_list with no releases, returns not found", func(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("call tool failed %v", err)
|
||||
}
|
||||
if toolResult.IsError {
|
||||
t.Fatalf("call tool failed")
|
||||
}
|
||||
if toolResult.Content[0].(mcp.TextContent).Text != "No Helm releases found" {
|
||||
t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
|
||||
}
|
||||
})
|
||||
_, err = kc.CoreV1().Secrets("default").Create(c.ctx, &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "release-to-list",
|
||||
Labels: map[string]string{"owner": "helm"},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"release": []byte(base64.StdEncoding.EncodeToString([]byte("{" +
|
||||
"\"name\":\"release-to-list\"," +
|
||||
"\"info\":{\"status\":\"deployed\"}" +
|
||||
"}"))),
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
toolResult, err = c.callTool("helm_list", map[string]interface{}{})
|
||||
t.Run("helm_list with deployed release, returns release", func(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("call tool failed %v", err)
|
||||
}
|
||||
if toolResult.IsError {
|
||||
t.Fatalf("call tool failed")
|
||||
}
|
||||
var decoded []map[string]interface{}
|
||||
err = yaml.Unmarshal([]byte(toolResult.Content[0].(mcp.TextContent).Text), &decoded)
|
||||
if err != nil {
|
||||
t.Fatalf("invalid tool result content %v", err)
|
||||
}
|
||||
if len(decoded) != 1 {
|
||||
t.Fatalf("invalid helm list count, expected 1, got %v", len(decoded))
|
||||
}
|
||||
if decoded[0]["name"] != "release-to-list" {
|
||||
t.Fatalf("invalid helm list name, expected release-to-list, got %v", decoded[0]["name"])
|
||||
}
|
||||
if decoded[0]["info"].(map[string]interface{})["status"] != "deployed" {
|
||||
t.Fatalf("invalid helm list status, expected deployed, got %v", decoded[0]["info"].(map[string]interface{})["status"])
|
||||
}
|
||||
})
|
||||
toolResult, err = c.callTool("helm_list", map[string]interface{}{"namespace": "ns-1"})
|
||||
t.Run("helm_list with deployed release in other namespaces, returns not found", func(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("call tool failed %v", err)
|
||||
}
|
||||
if toolResult.IsError {
|
||||
t.Fatalf("call tool failed")
|
||||
}
|
||||
if toolResult.Content[0].(mcp.TextContent).Text != "No Helm releases found" {
|
||||
t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
|
||||
}
|
||||
})
|
||||
toolResult, err = c.callTool("helm_list", map[string]interface{}{"namespace": "ns-1", "all_namespaces": true})
|
||||
t.Run("helm_list with deployed release in all namespaces, returns release", func(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("call tool failed %v", err)
|
||||
}
|
||||
if toolResult.IsError {
|
||||
t.Fatalf("call tool failed")
|
||||
}
|
||||
var decoded []map[string]interface{}
|
||||
err = yaml.Unmarshal([]byte(toolResult.Content[0].(mcp.TextContent).Text), &decoded)
|
||||
if err != nil {
|
||||
t.Fatalf("invalid tool result content %v", err)
|
||||
}
|
||||
if len(decoded) != 1 {
|
||||
t.Fatalf("invalid helm list count, expected 1, got %v", len(decoded))
|
||||
}
|
||||
if decoded[0]["name"] != "release-to-list" {
|
||||
t.Fatalf("invalid helm list name, expected release-to-list, got %v", decoded[0]["name"])
|
||||
}
|
||||
if decoded[0]["info"].(map[string]interface{})["status"] != "deployed" {
|
||||
t.Fatalf("invalid helm list status, expected deployed, got %v", decoded[0]["info"].(map[string]interface{})["status"])
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"github.com/manusa/kubernetes-mcp-server/pkg/helm"
|
||||
"github.com/manusa/kubernetes-mcp-server/pkg/kubernetes"
|
||||
"github.com/manusa/kubernetes-mcp-server/pkg/version"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
@@ -17,7 +16,6 @@ type Server struct {
|
||||
configuration *Configuration
|
||||
server *server.MCPServer
|
||||
k *kubernetes.Kubernetes
|
||||
helm *helm.Helm
|
||||
}
|
||||
|
||||
func NewSever(configuration Configuration) (*Server, error) {
|
||||
@@ -35,13 +33,6 @@ func NewSever(configuration Configuration) (*Server, error) {
|
||||
if err := s.reloadKubernetesClient(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// After Kubernetes client is initialized, set up Helm with the same config
|
||||
if s.k != nil {
|
||||
kubeconfig := s.k.KubeconfigPath()
|
||||
kubeContext := s.k.CurrentContext()
|
||||
namespace := s.k.ConfiguredNamespace()
|
||||
s.helm = helm.NewHelm(kubeconfig, kubeContext, namespace)
|
||||
}
|
||||
s.k.WatchKubeConfig(s.reloadKubernetesClient)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ func TestTools(t *testing.T) {
|
||||
expectedNames := []string{
|
||||
"configuration_view",
|
||||
"events_list",
|
||||
"helm_list",
|
||||
"namespaces_list",
|
||||
"pods_list",
|
||||
"pods_list_in_namespace",
|
||||
@@ -61,6 +62,7 @@ func TestTools(t *testing.T) {
|
||||
"pods_delete",
|
||||
"pods_log",
|
||||
"pods_run",
|
||||
"pods_exec",
|
||||
"resources_list",
|
||||
"resources_get",
|
||||
"resources_create_or_update",
|
||||
|
||||
Reference in New Issue
Block a user