Skip to content

Commit 6afb60f

Browse files
authored
feat(helm): support for helm uninstall
1 parent f94de90 commit 6afb60f

File tree

5 files changed

+122
-10
lines changed

5 files changed

+122
-10
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ A powerful and flexible Kubernetes [Model Context Protocol (MCP)](https://blog.m
3131
- **☸️ Helm**:
3232
- **Install** a Helm chart in the current or provided namespace.
3333
- **List** Helm releases in all namespaces or in a specific namespace.
34+
- **Uninstall** a Helm release in the current or provided namespace.
3435

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

@@ -195,6 +196,17 @@ List all the Helm releases in the current or provided namespace (or in all names
195196
- If `true`, will list Helm releases from all namespaces
196197
- If `false`, will list Helm releases from the specified namespace
197198

199+
### `helm_uninstall`
200+
201+
Uninstall a Helm release in the current or provided namespace with the provided name
202+
203+
**Parameters:**
204+
- `name` (`string`, required)
205+
- Name of the Helm release to uninstall
206+
- `namespace` (`string`, optional)
207+
- Namespace to uninstall the Helm release from
208+
- If not provided, will use the configured namespace
209+
198210
### `namespaces_list`
199211

200212
List all the Kubernetes namespaces in the current cluster

pkg/helm/helm.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package helm
22

33
import (
44
"context"
5+
"fmt"
56
"helm.sh/helm/v3/pkg/action"
67
"helm.sh/helm/v3/pkg/chart/loader"
78
"helm.sh/helm/v3/pkg/cli"
@@ -28,7 +29,7 @@ func NewHelm(kubernetes Kubernetes) *Helm {
2829
}
2930

3031
func (h *Helm) Install(ctx context.Context, chart string, values map[string]interface{}, name string, namespace string) (string, error) {
31-
cfg, err := h.newAction(namespace, false)
32+
cfg, err := h.newAction(h.kubernetes.NamespaceOrDefault(namespace), false)
3233
if err != nil {
3334
return "", err
3435
}
@@ -82,6 +83,24 @@ func (h *Helm) List(namespace string, allNamespaces bool) (string, error) {
8283
return string(ret), nil
8384
}
8485

86+
func (h *Helm) Uninstall(name string, namespace string) (string, error) {
87+
cfg, err := h.newAction(h.kubernetes.NamespaceOrDefault(namespace), false)
88+
if err != nil {
89+
return "", err
90+
}
91+
uninstall := action.NewUninstall(cfg)
92+
uninstall.IgnoreNotFound = true
93+
uninstall.Wait = true
94+
uninstall.Timeout = 5 * time.Minute
95+
uninstalledRelease, err := uninstall.Run(name)
96+
if uninstalledRelease == nil && err == nil {
97+
return fmt.Sprintf("Release %s not found", name), nil
98+
} else if err != nil {
99+
return "", err
100+
}
101+
return fmt.Sprintf("Uninstalled release %s %s", uninstalledRelease.Release.Name, uninstalledRelease.Info), nil
102+
}
103+
85104
func (h *Helm) newAction(namespace string, allNamespaces bool) (*action.Configuration, error) {
86105
cfg := new(action.Configuration)
87106
applicableNamespace := ""

pkg/mcp/helm.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ func (s *Server) initHelm() []server.ServerTool {
2121
mcp.WithString("namespace", mcp.Description("Namespace to list Helm releases from (Optional, all namespaces if not provided)")),
2222
mcp.WithBoolean("all_namespaces", mcp.Description("If true, lists all Helm releases in all namespaces ignoring the namespace argument (Optional)")),
2323
), s.helmList},
24+
{mcp.NewTool("helm_uninstall",
25+
mcp.WithDescription("Uninstall a Helm release in the current or provided namespace"),
26+
mcp.WithString("name", mcp.Description("Name of the Helm release to uninstall"), mcp.Required()),
27+
mcp.WithString("namespace", mcp.Description("Namespace to uninstall the Helm release from (Optional, current namespace if not provided)")),
28+
), s.helmUninstall},
2429
}
2530
}
2631

@@ -64,3 +69,20 @@ func (s *Server) helmList(_ context.Context, ctr mcp.CallToolRequest) (*mcp.Call
6469
}
6570
return NewTextResult(ret, err), nil
6671
}
72+
73+
func (s *Server) helmUninstall(_ context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
74+
var name string
75+
ok := false
76+
if name, ok = ctr.Params.Arguments["name"].(string); !ok {
77+
return NewTextResult("", fmt.Errorf("failed to uninstall helm chart, missing argument name")), nil
78+
}
79+
namespace := ""
80+
if v, ok := ctr.Params.Arguments["namespace"].(string); ok {
81+
namespace = v
82+
}
83+
ret, err := s.k.Helm.Uninstall(name, namespace)
84+
if err != nil {
85+
return NewTextResult("", fmt.Errorf("failed to uninstall helm chart '%s': %w", name, err)), nil
86+
}
87+
return NewTextResult(ret, err), nil
88+
}

pkg/mcp/helm_test.go

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package mcp
22

33
import (
4+
"context"
45
"encoding/base64"
56
"github.com/mark3labs/mcp-go/mcp"
67
corev1 "k8s.io/api/core/v1"
8+
"k8s.io/apimachinery/pkg/api/errors"
79
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"k8s.io/client-go/kubernetes"
811
"path/filepath"
912
"runtime"
1013
"sigs.k8s.io/yaml"
@@ -58,13 +61,7 @@ func TestHelmList(t *testing.T) {
5861
testCase(t, func(c *mcpContext) {
5962
c.withEnvTest()
6063
kc := c.newKubernetesClient()
61-
secrets, err := kc.CoreV1().Secrets("default").List(c.ctx, metav1.ListOptions{})
62-
for _, secret := range secrets.Items {
63-
if strings.HasPrefix(secret.Name, "sh.helm.release.v1.") {
64-
_ = kc.CoreV1().Secrets("default").Delete(c.ctx, secret.Name, metav1.DeleteOptions{})
65-
}
66-
}
67-
_ = kc.CoreV1().Secrets("default").Delete(c.ctx, "release-to-list", metav1.DeleteOptions{})
64+
clearHelmReleases(c.ctx, kc)
6865
toolResult, err := c.callTool("helm_list", map[string]interface{}{})
6966
t.Run("helm_list with no releases, returns not found", func(t *testing.T) {
7067
if err != nil {
@@ -79,8 +76,8 @@ func TestHelmList(t *testing.T) {
7976
})
8077
_, _ = kc.CoreV1().Secrets("default").Create(c.ctx, &corev1.Secret{
8178
ObjectMeta: metav1.ObjectMeta{
82-
Name: "release-to-list",
83-
Labels: map[string]string{"owner": "helm"},
79+
Name: "sh.helm.release.v1.release-to-list",
80+
Labels: map[string]string{"owner": "helm", "name": "release-to-list"},
8481
},
8582
Data: map[string][]byte{
8683
"release": []byte(base64.StdEncoding.EncodeToString([]byte("{" +
@@ -149,3 +146,64 @@ func TestHelmList(t *testing.T) {
149146
})
150147
})
151148
}
149+
150+
func TestHelmUninstall(t *testing.T) {
151+
testCase(t, func(c *mcpContext) {
152+
c.withEnvTest()
153+
kc := c.newKubernetesClient()
154+
clearHelmReleases(c.ctx, kc)
155+
toolResult, err := c.callTool("helm_uninstall", map[string]interface{}{
156+
"name": "release-to-uninstall",
157+
})
158+
t.Run("helm_uninstall with no releases, returns not found", func(t *testing.T) {
159+
if err != nil {
160+
t.Fatalf("call tool failed %v", err)
161+
}
162+
if toolResult.IsError {
163+
t.Fatalf("call tool failed")
164+
}
165+
if toolResult.Content[0].(mcp.TextContent).Text != "Release release-to-uninstall not found" {
166+
t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
167+
}
168+
})
169+
_, _ = kc.CoreV1().Secrets("default").Create(c.ctx, &corev1.Secret{
170+
ObjectMeta: metav1.ObjectMeta{
171+
Name: "sh.helm.release.v1.existent-release-to-uninstall.v0",
172+
Labels: map[string]string{"owner": "helm", "name": "existent-release-to-uninstall"},
173+
},
174+
Data: map[string][]byte{
175+
"release": []byte(base64.StdEncoding.EncodeToString([]byte("{" +
176+
"\"name\":\"existent-release-to-uninstall\"," +
177+
"\"info\":{\"status\":\"deployed\"}" +
178+
"}"))),
179+
},
180+
}, metav1.CreateOptions{})
181+
toolResult, err = c.callTool("helm_uninstall", map[string]interface{}{
182+
"name": "existent-release-to-uninstall",
183+
})
184+
t.Run("helm_uninstall with deployed release, returns uninstalled", func(t *testing.T) {
185+
if err != nil {
186+
t.Fatalf("call tool failed %v", err)
187+
}
188+
if toolResult.IsError {
189+
t.Fatalf("call tool failed")
190+
}
191+
if !strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "Uninstalled release existent-release-to-uninstall") {
192+
t.Fatalf("unexpected result %v", toolResult.Content[0].(mcp.TextContent).Text)
193+
}
194+
_, err = kc.CoreV1().Secrets("default").Get(c.ctx, "sh.helm.release.v1.existent-release-to-uninstall.v0", metav1.GetOptions{})
195+
if !errors.IsNotFound(err) {
196+
t.Fatalf("expected release to be deleted, but it still exists")
197+
}
198+
})
199+
})
200+
}
201+
202+
func clearHelmReleases(ctx context.Context, kc *kubernetes.Clientset) {
203+
secrets, _ := kc.CoreV1().Secrets("default").List(ctx, metav1.ListOptions{})
204+
for _, secret := range secrets.Items {
205+
if strings.HasPrefix(secret.Name, "sh.helm.release.v1.") {
206+
_ = kc.CoreV1().Secrets("default").Delete(ctx, secret.Name, metav1.DeleteOptions{})
207+
}
208+
}
209+
}

pkg/mcp/mcp_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ func TestTools(t *testing.T) {
5656
"events_list",
5757
"helm_install",
5858
"helm_list",
59+
"helm_uninstall",
5960
"namespaces_list",
6061
"pods_list",
6162
"pods_list_in_namespace",

0 commit comments

Comments
 (0)