Skip to content

Commit 2f36de5

Browse files
committed
refactor(helm): adapt Helm contribution to project structure
1 parent ff8381f commit 2f36de5

File tree

12 files changed

+200
-171
lines changed

12 files changed

+200
-171
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ A powerful and flexible Kubernetes [Model Context Protocol (MCP)](https://blog.m
2828
- **✅ Namespaces**: List Kubernetes Namespaces.
2929
- **✅ Events**: View Kubernetes events in all namespaces or in a specific namespace.
3030
- **✅ Projects**: List OpenShift Projects.
31+
- **☸️ Helm**:
32+
- **List** Helm releases in all namespaces or in a specific namespace.
3133

3234
Unlike other Kubernetes MCP server implementations, this **IS NOT** just a wrapper around `kubectl` or `helm` command-line tools.
3335

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ require (
99
github.com/spf13/cobra v1.9.1
1010
github.com/spf13/viper v1.20.1
1111
golang.org/x/net v0.40.0
12-
gopkg.in/yaml.v3 v3.0.1
1312
helm.sh/helm/v3 v3.17.3
1413
k8s.io/api v0.33.0
1514
k8s.io/apiextensions-apiserver v0.33.0
1615
k8s.io/apimachinery v0.33.0
16+
k8s.io/cli-runtime v0.32.2
1717
k8s.io/client-go v0.33.0
1818
k8s.io/klog/v2 v2.130.1
1919
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
@@ -145,8 +145,8 @@ require (
145145
google.golang.org/protobuf v1.36.5 // indirect
146146
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
147147
gopkg.in/inf.v0 v0.9.1 // indirect
148+
gopkg.in/yaml.v3 v3.0.1 // indirect
148149
k8s.io/apiserver v0.33.0 // indirect
149-
k8s.io/cli-runtime v0.32.2 // indirect
150150
k8s.io/component-base v0.33.0 // indirect
151151
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
152152
k8s.io/kubectl v0.32.2 // indirect

pkg/helm/helm.go

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,52 @@
11
package helm
22

33
import (
4-
"context"
5-
"log"
6-
74
"helm.sh/helm/v3/pkg/action"
85
"helm.sh/helm/v3/pkg/cli"
9-
"helm.sh/helm/v3/pkg/release"
6+
"k8s.io/cli-runtime/pkg/genericclioptions"
7+
"log"
8+
"sigs.k8s.io/yaml"
109
)
1110

12-
// Helm provides methods to interact with Helm releases
13-
// Mirrors the abstraction style of pkg/kubernetes
11+
type Kubernetes interface {
12+
genericclioptions.RESTClientGetter
13+
NamespaceOrDefault(namespace string) string
14+
}
1415

1516
type Helm struct {
16-
settings *cli.EnvSettings
17+
kubernetes Kubernetes
1718
}
1819

19-
// NewHelm creates a new Helm instance using kubeconfig, context, and namespace settings
20-
func NewHelm(kubeconfig, kubeContext, namespace string) *Helm {
20+
// NewHelm creates a new Helm instance
21+
func NewHelm(kubernetes Kubernetes, namespace string) *Helm {
2122
settings := cli.New()
22-
if kubeconfig != "" {
23-
settings.KubeConfig = kubeconfig
24-
}
25-
if kubeContext != "" {
26-
settings.KubeContext = kubeContext
27-
}
2823
if namespace != "" {
2924
settings.SetNamespace(namespace)
3025
}
31-
return &Helm{settings: settings}
26+
return &Helm{kubernetes: kubernetes}
3227
}
3328

34-
// ReleasesList lists Helm releases in a specific namespace (or all namespaces if namespace is empty)
35-
func (h *Helm) ReleasesList(ctx context.Context, namespace string) ([]*release.Release, error) {
36-
// If no namespace is given, use the default from kubeconfig
37-
if namespace == "" {
38-
namespace = h.settings.Namespace()
39-
}
29+
// ReleasesList lists all the releases for the specified namespace (or current namespace if). Or allNamespaces is true, it lists all releases across all namespaces.
30+
func (h *Helm) ReleasesList(namespace string, allNamespaces bool) (string, error) {
4031
cfg := new(action.Configuration)
41-
if err := cfg.Init(h.settings.RESTClientGetter(), namespace, "", log.Printf); err != nil {
42-
return nil, err
32+
applicableNamespace := ""
33+
if !allNamespaces {
34+
applicableNamespace = h.kubernetes.NamespaceOrDefault(namespace)
35+
}
36+
if err := cfg.Init(h.kubernetes, applicableNamespace, "", log.Printf); err != nil {
37+
return "", err
4338
}
4439
list := action.NewList(cfg)
45-
// To list across all namespaces, set AllNamespaces to true
46-
if namespace == "" || namespace == "all" {
47-
list.AllNamespaces = true
40+
list.AllNamespaces = allNamespaces
41+
releases, err := list.Run()
42+
if err != nil {
43+
return "", err
44+
} else if len(releases) == 0 {
45+
return "No Helm releases found", nil
46+
}
47+
ret, err := yaml.Marshal(releases)
48+
if err != nil {
49+
return "", err
4850
}
49-
return list.Run()
51+
return string(ret), nil
5052
}

pkg/helm/helm_test.go

Lines changed: 0 additions & 40 deletions
This file was deleted.

pkg/kubernetes/configuration.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,30 @@ func (k *Kubernetes) IsInCluster() bool {
5252
return err == nil && cfg != nil
5353
}
5454

55+
func (k *Kubernetes) configuredNamespace() string {
56+
if ns, _, nsErr := k.clientCmdConfig.Namespace(); nsErr == nil {
57+
return ns
58+
}
59+
return ""
60+
}
61+
62+
func (k *Kubernetes) NamespaceOrDefault(namespace string) string {
63+
if namespace == "" {
64+
return k.configuredNamespace()
65+
}
66+
return namespace
67+
}
68+
69+
// ToRESTConfig returns the rest.Config object (genericclioptions.RESTClientGetter)
70+
func (k *Kubernetes) ToRESTConfig() (*rest.Config, error) {
71+
return k.cfg, nil
72+
}
73+
74+
// ToRawKubeConfigLoader returns the clientcmd.ClientConfig object (genericclioptions.RESTClientGetter)
75+
func (k *Kubernetes) ToRawKubeConfigLoader() clientcmd.ClientConfig {
76+
return k.clientCmdConfig
77+
}
78+
5579
func (k *Kubernetes) ConfigurationView(minify bool) (string, error) {
5680
var cfg clientcmdapi.Config
5781
var err error

pkg/kubernetes/kubernetes.go

Lines changed: 15 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package kubernetes
22

33
import (
44
"github.com/fsnotify/fsnotify"
5+
"github.com/manusa/kubernetes-mcp-server/pkg/helm"
56
v1 "k8s.io/api/core/v1"
7+
"k8s.io/apimachinery/pkg/api/meta"
68
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
79
"k8s.io/apimachinery/pkg/runtime"
810
"k8s.io/client-go/discovery"
@@ -26,9 +28,10 @@ type Kubernetes struct {
2628
scheme *runtime.Scheme
2729
parameterCodec runtime.ParameterCodec
2830
clientSet kubernetes.Interface
29-
discoveryClient *discovery.DiscoveryClient
31+
discoveryClient discovery.CachedDiscoveryInterface
3032
deferredDiscoveryRESTMapper *restmapper.DeferredDiscoveryRESTMapper
3133
dynamicClient *dynamic.DynamicClient
34+
Helm *helm.Helm
3235
}
3336

3437
func NewKubernetes(kubeconfig string) (*Kubernetes, error) {
@@ -43,10 +46,11 @@ func NewKubernetes(kubeconfig string) (*Kubernetes, error) {
4346
if err != nil {
4447
return nil, err
4548
}
46-
k8s.discoveryClient, err = discovery.NewDiscoveryClientForConfig(k8s.cfg)
49+
discoveryClient, err := discovery.NewDiscoveryClientForConfig(k8s.cfg)
4750
if err != nil {
4851
return nil, err
4952
}
53+
k8s.discoveryClient = memory.NewMemCacheClient(discoveryClient)
5054
k8s.deferredDiscoveryRESTMapper = restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(k8s.discoveryClient))
5155
k8s.dynamicClient, err = dynamic.NewForConfig(k8s.cfg)
5256
if err != nil {
@@ -57,6 +61,7 @@ func NewKubernetes(kubeconfig string) (*Kubernetes, error) {
5761
return nil, err
5862
}
5963
k8s.parameterCodec = runtime.NewParameterCodec(k8s.scheme)
64+
k8s.Helm = helm.NewHelm(k8s, "TODO")
6065
return k8s, nil
6166
}
6267

@@ -102,6 +107,14 @@ func (k *Kubernetes) Close() {
102107
}
103108
}
104109

110+
func (k *Kubernetes) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
111+
return k.discoveryClient, nil
112+
}
113+
114+
func (k *Kubernetes) ToRESTMapper() (meta.RESTMapper, error) {
115+
return k.deferredDiscoveryRESTMapper, nil
116+
}
117+
105118
func marshal(v any) (string, error) {
106119
switch t := v.(type) {
107120
case []unstructured.Unstructured:
@@ -123,37 +136,3 @@ func marshal(v any) (string, error) {
123136
}
124137
return string(ret), nil
125138
}
126-
127-
// KubeconfigPath returns the kubeconfig path used by this Kubernetes client
128-
func (k *Kubernetes) KubeconfigPath() string {
129-
return k.Kubeconfig
130-
}
131-
132-
// CurrentContext returns the current context from the kubeconfig
133-
func (k *Kubernetes) CurrentContext() string {
134-
if k.clientCmdConfig == nil {
135-
return ""
136-
}
137-
if rawConfig, err := k.clientCmdConfig.RawConfig(); err == nil {
138-
return rawConfig.CurrentContext
139-
}
140-
return ""
141-
}
142-
143-
// ConfiguredNamespace returns the namespace configured in the kubeconfig/context
144-
func (k *Kubernetes) ConfiguredNamespace() string {
145-
if k.clientCmdConfig == nil {
146-
return ""
147-
}
148-
if ns, _, nsErr := k.clientCmdConfig.Namespace(); nsErr == nil {
149-
return ns
150-
}
151-
return ""
152-
}
153-
154-
func (k *Kubernetes) namespaceOrDefault(namespace string) string {
155-
if namespace == "" {
156-
return k.ConfiguredNamespace()
157-
}
158-
return namespace
159-
}

pkg/kubernetes/pods.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ func (k *Kubernetes) PodsListInNamespace(ctx context.Context, namespace string)
3232
func (k *Kubernetes) PodsGet(ctx context.Context, namespace, name string) (string, error) {
3333
return k.ResourcesGet(ctx, &schema.GroupVersionKind{
3434
Group: "", Version: "v1", Kind: "Pod",
35-
}, k.namespaceOrDefault(namespace), name)
35+
}, k.NamespaceOrDefault(namespace), name)
3636
}
3737

3838
func (k *Kubernetes) PodsDelete(ctx context.Context, namespace, name string) (string, error) {
39-
namespace = k.namespaceOrDefault(namespace)
39+
namespace = k.NamespaceOrDefault(namespace)
4040
pod, err := k.clientSet.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{})
4141
if err != nil {
4242
return "", err
@@ -79,7 +79,7 @@ func (k *Kubernetes) PodsDelete(ctx context.Context, namespace, name string) (st
7979

8080
func (k *Kubernetes) PodsLog(ctx context.Context, namespace, name, container string) (string, error) {
8181
tailLines := int64(256)
82-
req := k.clientSet.CoreV1().Pods(k.namespaceOrDefault(namespace)).GetLogs(name, &v1.PodLogOptions{
82+
req := k.clientSet.CoreV1().Pods(k.NamespaceOrDefault(namespace)).GetLogs(name, &v1.PodLogOptions{
8383
TailLines: &tailLines,
8484
Container: container,
8585
})
@@ -108,7 +108,7 @@ func (k *Kubernetes) PodsRun(ctx context.Context, namespace, name, image string,
108108
var resources []any
109109
pod := &v1.Pod{
110110
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"},
111-
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: k.namespaceOrDefault(namespace), Labels: labels},
111+
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: k.NamespaceOrDefault(namespace), Labels: labels},
112112
Spec: v1.PodSpec{Containers: []v1.Container{{
113113
Name: name,
114114
Image: image,
@@ -120,7 +120,7 @@ func (k *Kubernetes) PodsRun(ctx context.Context, namespace, name, image string,
120120
pod.Spec.Containers[0].Ports = []v1.ContainerPort{{ContainerPort: port}}
121121
resources = append(resources, &v1.Service{
122122
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Service"},
123-
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: k.namespaceOrDefault(namespace), Labels: labels},
123+
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: k.NamespaceOrDefault(namespace), Labels: labels},
124124
Spec: v1.ServiceSpec{
125125
Selector: labels,
126126
Type: v1.ServiceTypeClusterIP,
@@ -135,7 +135,7 @@ func (k *Kubernetes) PodsRun(ctx context.Context, namespace, name, image string,
135135
"kind": "Route",
136136
"metadata": map[string]interface{}{
137137
"name": name,
138-
"namespace": k.namespaceOrDefault(namespace),
138+
"namespace": k.NamespaceOrDefault(namespace),
139139
"labels": labels,
140140
},
141141
"spec": map[string]interface{}{
@@ -175,7 +175,7 @@ func (k *Kubernetes) PodsRun(ctx context.Context, namespace, name, image string,
175175
}
176176

177177
func (k *Kubernetes) PodsExec(ctx context.Context, namespace, name, container string, command []string) (string, error) {
178-
namespace = k.namespaceOrDefault(namespace)
178+
namespace = k.NamespaceOrDefault(namespace)
179179
pod, err := k.clientSet.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{})
180180
if err != nil {
181181
return "", err

pkg/kubernetes/resources.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func (k *Kubernetes) ResourcesGet(ctx context.Context, gvk *schema.GroupVersionK
3434
}
3535
// If it's a namespaced resource and namespace wasn't provided, try to use the default configured one
3636
if namespaced, nsErr := k.isNamespaced(gvk); nsErr == nil && namespaced {
37-
namespace = k.namespaceOrDefault(namespace)
37+
namespace = k.NamespaceOrDefault(namespace)
3838
}
3939
rg, err := k.dynamicClient.Resource(*gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
4040
if err != nil {
@@ -64,7 +64,7 @@ func (k *Kubernetes) ResourcesDelete(ctx context.Context, gvk *schema.GroupVersi
6464
}
6565
// If it's a namespaced resource and namespace wasn't provided, try to use the default configured one
6666
if namespaced, nsErr := k.isNamespaced(gvk); nsErr == nil && namespaced {
67-
namespace = k.namespaceOrDefault(namespace)
67+
namespace = k.NamespaceOrDefault(namespace)
6868
}
6969
return k.dynamicClient.Resource(*gvr).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{})
7070
}
@@ -77,7 +77,7 @@ func (k *Kubernetes) resourcesList(ctx context.Context, gvk *schema.GroupVersion
7777
// Check if operation is allowed for all namespaces (applicable for namespaced resources)
7878
isNamespaced, _ := k.isNamespaced(gvk)
7979
if isNamespaced && !k.canIUse(ctx, gvr, namespace, "list") && namespace == "" {
80-
namespace = k.ConfiguredNamespace()
80+
namespace = k.configuredNamespace()
8181
}
8282
return k.dynamicClient.Resource(*gvr).Namespace(namespace).List(ctx, metav1.ListOptions{})
8383
}
@@ -92,7 +92,7 @@ func (k *Kubernetes) resourcesCreateOrUpdate(ctx context.Context, resources []*u
9292
namespace := obj.GetNamespace()
9393
// If it's a namespaced resource and namespace wasn't provided, try to use the default configured one
9494
if namespaced, nsErr := k.isNamespaced(&gvk); nsErr == nil && namespaced {
95-
namespace = k.namespaceOrDefault(namespace)
95+
namespace = k.NamespaceOrDefault(namespace)
9696
}
9797
resources[i], rErr = k.dynamicClient.Resource(*gvr).Namespace(namespace).Apply(ctx, obj.GetName(), obj, metav1.ApplyOptions{
9898
FieldManager: version.BinaryName,

0 commit comments

Comments
 (0)