Skip to content

Commit ff6d526

Browse files
committed
feat: add operations for helm
Signed-off-by: Matthias Wessendorf <mwessend@redhat.com>
1 parent 6e67dd3 commit ff6d526

4 files changed

Lines changed: 530 additions & 0 deletions

File tree

README.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ This extension enables declarative Kubernetes interactions within mcpchecker tas
1212
| `kubernetes.create` | Create a Kubernetes resource |
1313
| `kubernetes.delete` | Delete a Kubernetes resource |
1414
| `kubernetes.getCurrentContext` | Get the current context from kubeconfig |
15+
| `kubernetes.helmInstall` | Install a Helm chart as a release |
16+
| `kubernetes.helmList` | List Helm releases in a namespace or all namespaces |
17+
| `kubernetes.helmUninstall` | Uninstall a Helm release |
1518
| `kubernetes.listContexts` | List all contexts from kubeconfig |
1619
| `kubernetes.viewConfig` | View kubeconfig as YAML (optionally minified) |
1720
| `kubernetes.wait` | Wait for a condition on a resource (e.g., `Ready`, `Available`) |
@@ -87,6 +90,52 @@ spec:
8790
inline: Create an nginx pod named web-server in the test-namespace namespace
8891
```
8992

93+
### Helm Example
94+
95+
Test Helm operations using declarative setup and cleanup:
96+
97+
```yaml
98+
kind: Task
99+
apiVersion: gevals/v1alpha2
100+
metadata:
101+
name: "list-helm-releases"
102+
difficulty: easy
103+
spec:
104+
requires:
105+
- extension: kubernetes
106+
107+
setup:
108+
# Create namespace
109+
- kubernetes.create:
110+
apiVersion: v1
111+
kind: Namespace
112+
metadata:
113+
name: helm-test
114+
115+
# Install a test release
116+
- kubernetes.helmInstall:
117+
chart: oci://registry-1.docker.io/bitnamicharts/nginx
118+
name: test-nginx
119+
namespace: helm-test
120+
121+
cleanup:
122+
# Uninstall the release
123+
- kubernetes.helmUninstall:
124+
name: test-nginx
125+
namespace: helm-test
126+
127+
# Delete namespace
128+
- kubernetes.delete:
129+
apiVersion: v1
130+
kind: Namespace
131+
metadata:
132+
name: helm-test
133+
ignoreNotFound: true
134+
135+
prompt:
136+
inline: List all Helm releases in the cluster
137+
```
138+
90139
## Operation Reference
91140

92141
### kubernetes.create
@@ -135,6 +184,48 @@ Waits for a condition on a resource. Supports configurable timeout and expected
135184
timeout: 5m # optional, defaults to 60s
136185
```
137186

187+
### kubernetes.helmInstall
188+
189+
Installs a Helm chart as a release. Supports chart repositories and OCI registries.
190+
191+
```yaml
192+
- kubernetes.helmInstall:
193+
chart: oci://registry-1.docker.io/bitnamicharts/nginx
194+
name: my-nginx # optional, generates name if not provided
195+
namespace: default # optional
196+
values: # optional Helm values
197+
replicaCount: 2
198+
service:
199+
type: LoadBalancer
200+
```
201+
202+
### kubernetes.helmList
203+
204+
Lists Helm releases in a namespace or across all namespaces.
205+
206+
```yaml
207+
# List in specific namespace
208+
- kubernetes.helmList:
209+
namespace: default
210+
211+
# List across all namespaces
212+
- kubernetes.helmList:
213+
allNamespaces: true
214+
```
215+
216+
**Outputs:**
217+
- `releases`: Information about found Helm releases (name, namespace, status, chart)
218+
219+
### kubernetes.helmUninstall
220+
221+
Uninstalls a Helm release. Gracefully handles releases that don't exist.
222+
223+
```yaml
224+
- kubernetes.helmUninstall:
225+
name: my-nginx
226+
namespace: default # optional
227+
```
228+
138229
### kubernetes.listContexts
139230

140231
Lists all contexts from the kubeconfig file, including which one is currently active.

pkg/extension/helm.go

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package extension
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"os/exec"
8+
"strings"
9+
10+
"github.com/mcpchecker/mcpchecker/pkg/extension/sdk"
11+
)
12+
13+
// handleHelmInstall installs a Helm chart as a release
14+
func (e *Extension) handleHelmInstall(ctx context.Context, req *sdk.OperationRequest) (*sdk.OperationResult, error) {
15+
args, ok := req.Args.(map[string]any)
16+
if !ok {
17+
return sdk.Failure(fmt.Errorf("args must be an object")), nil
18+
}
19+
20+
chart, ok := args["chart"].(string)
21+
if !ok || chart == "" {
22+
return sdk.Failure(fmt.Errorf("chart parameter is required")), nil
23+
}
24+
25+
// Optional parameters
26+
name, _ := args["name"].(string)
27+
namespace, _ := args["namespace"].(string)
28+
values, _ := args["values"].(map[string]interface{})
29+
30+
cmdArgs := []string{"install"}
31+
32+
if name != "" {
33+
cmdArgs = append(cmdArgs, name)
34+
} else {
35+
cmdArgs = append(cmdArgs, "--generate-name")
36+
}
37+
38+
cmdArgs = append(cmdArgs, chart)
39+
40+
if namespace != "" {
41+
cmdArgs = append(cmdArgs, "--namespace", namespace)
42+
}
43+
44+
// Add values as --set flags
45+
for k, v := range values {
46+
cmdArgs = append(cmdArgs, "--set", fmt.Sprintf("%s=%v", k, v))
47+
}
48+
49+
e.LogInfo(ctx, "Installing Helm chart", map[string]any{
50+
"chart": chart,
51+
"name": name,
52+
"namespace": namespace,
53+
})
54+
55+
cmd := exec.CommandContext(ctx, "helm", cmdArgs...)
56+
output, err := cmd.CombinedOutput()
57+
if err != nil {
58+
e.LogError(ctx, "Helm install failed", map[string]any{
59+
"chart": chart,
60+
"error": err.Error(),
61+
})
62+
return sdk.Failure(fmt.Errorf("helm install failed: %s\nOutput: %s", err, string(output))), nil
63+
}
64+
65+
e.LogInfo(ctx, "Helm chart installed successfully", map[string]any{
66+
"chart": chart,
67+
"name": name,
68+
})
69+
70+
return sdk.Success(fmt.Sprintf("Helm chart installed successfully\n%s", string(output))), nil
71+
}
72+
73+
// handleHelmList lists Helm releases
74+
func (e *Extension) handleHelmList(ctx context.Context, req *sdk.OperationRequest) (*sdk.OperationResult, error) {
75+
args, ok := req.Args.(map[string]any)
76+
if !ok {
77+
return sdk.Failure(fmt.Errorf("args must be an object")), nil
78+
}
79+
80+
cmdArgs := []string{"list", "--output", "json"}
81+
82+
namespace, _ := args["namespace"].(string)
83+
allNamespaces, _ := args["allNamespaces"].(bool)
84+
85+
if allNamespaces {
86+
cmdArgs = append(cmdArgs, "--all-namespaces")
87+
} else if namespace != "" {
88+
cmdArgs = append(cmdArgs, "--namespace", namespace)
89+
}
90+
91+
e.LogInfo(ctx, "Listing Helm releases", map[string]any{
92+
"namespace": namespace,
93+
"allNamespaces": allNamespaces,
94+
})
95+
96+
cmd := exec.CommandContext(ctx, "helm", cmdArgs...)
97+
output, err := cmd.CombinedOutput()
98+
if err != nil {
99+
e.LogError(ctx, "Helm list failed", map[string]any{
100+
"error": err.Error(),
101+
})
102+
return sdk.Failure(fmt.Errorf("helm list failed: %s\nOutput: %s", err, string(output))), nil
103+
}
104+
105+
// Parse JSON output
106+
var releases []map[string]interface{}
107+
if len(output) > 0 {
108+
if err := json.Unmarshal(output, &releases); err != nil {
109+
return sdk.Failure(fmt.Errorf("failed to parse helm list output: %s", err)), nil
110+
}
111+
}
112+
113+
if len(releases) == 0 {
114+
return sdk.Success("No Helm releases found"), nil
115+
}
116+
117+
// Format releases as a readable string
118+
var result strings.Builder
119+
result.WriteString(fmt.Sprintf("Found %d Helm release(s):\n", len(releases)))
120+
for _, release := range releases {
121+
name, _ := release["name"].(string)
122+
ns, _ := release["namespace"].(string)
123+
status, _ := release["status"].(string)
124+
chart, _ := release["chart"].(string)
125+
result.WriteString(fmt.Sprintf(" - %s (namespace: %s, status: %s, chart: %s)\n", name, ns, status, chart))
126+
}
127+
128+
return sdk.Success(result.String()), nil
129+
}
130+
131+
// handleHelmUninstall uninstalls a Helm release
132+
func (e *Extension) handleHelmUninstall(ctx context.Context, req *sdk.OperationRequest) (*sdk.OperationResult, error) {
133+
args, ok := req.Args.(map[string]any)
134+
if !ok {
135+
return sdk.Failure(fmt.Errorf("args must be an object")), nil
136+
}
137+
138+
name, ok := args["name"].(string)
139+
if !ok || name == "" {
140+
return sdk.Failure(fmt.Errorf("name parameter is required")), nil
141+
}
142+
143+
cmdArgs := []string{"uninstall", name}
144+
145+
namespace, _ := args["namespace"].(string)
146+
if namespace != "" {
147+
cmdArgs = append(cmdArgs, "--namespace", namespace)
148+
}
149+
150+
e.LogInfo(ctx, "Uninstalling Helm release", map[string]any{
151+
"name": name,
152+
"namespace": namespace,
153+
})
154+
155+
cmd := exec.CommandContext(ctx, "helm", cmdArgs...)
156+
output, err := cmd.CombinedOutput()
157+
if err != nil {
158+
// Check if it's a "not found" error
159+
if strings.Contains(string(output), "not found") {
160+
e.LogInfo(ctx, "Helm release not found (ignored)", map[string]any{
161+
"name": name,
162+
})
163+
return sdk.Success(fmt.Sprintf("Helm release '%s' not found (already uninstalled)", name)), nil
164+
}
165+
e.LogError(ctx, "Helm uninstall failed", map[string]any{
166+
"name": name,
167+
"error": err.Error(),
168+
})
169+
return sdk.Failure(fmt.Errorf("helm uninstall failed: %s\nOutput: %s", err, string(output))), nil
170+
}
171+
172+
e.LogInfo(ctx, "Helm release uninstalled successfully", map[string]any{
173+
"name": name,
174+
})
175+
176+
return sdk.Success(fmt.Sprintf("Helm release '%s' uninstalled successfully\n%s", name, string(output))), nil
177+
}

0 commit comments

Comments
 (0)