feat(kubernetes): resources_delete can get any resource in the cluster

This commit is contained in:
Marc Nuri
2025-02-17 13:17:48 +01:00
parent 3ea23f3d61
commit a8bb7c01a7
4 changed files with 173 additions and 0 deletions

View File

@@ -47,6 +47,10 @@ func (k *Kubernetes) ResourcesGet(ctx context.Context, gvk *schema.GroupVersionK
if err != nil {
return "", err
}
// 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 = namespaceOrDefault(namespace)
}
rg, err := client.Resource(*gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return "", err
@@ -68,6 +72,22 @@ func (k *Kubernetes) ResourcesCreateOrUpdate(ctx context.Context, resource strin
return k.resourcesCreateOrUpdate(ctx, parsedResources)
}
func (k *Kubernetes) ResourcesDelete(ctx context.Context, gvk *schema.GroupVersionKind, namespace, name string) error {
client, err := dynamic.NewForConfig(k.cfg)
if err != nil {
return err
}
gvr, err := k.resourceFor(gvk)
if err != nil {
return err
}
// 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 = namespaceOrDefault(namespace)
}
return client.Resource(*gvr).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{})
}
func (k *Kubernetes) resourcesCreateOrUpdate(ctx context.Context, resources []*unstructured.Unstructured) (string, error) {
client, err := dynamic.NewForConfig(k.cfg)
if err != nil {

View File

@@ -163,6 +163,8 @@ func createTestData(ctx context.Context, kc *kubernetes.Clientset) {
Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns-1"}}, metav1.CreateOptions{})
_, _ = kc.CoreV1().Namespaces().
Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns-2"}}, metav1.CreateOptions{})
_, _ = kc.CoreV1().Namespaces().
Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns-to-delete"}}, metav1.CreateOptions{})
_, _ = kc.CoreV1().Pods("default").
Create(ctx, &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "a-pod-in-default"},
@@ -190,4 +192,6 @@ func createTestData(ctx context.Context, kc *kubernetes.Clientset) {
},
},
}, metav1.CreateOptions{})
_, _ = kc.CoreV1().ConfigMaps("default").
Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "a-configmap-to-delete"}}, metav1.CreateOptions{})
}

View File

@@ -52,6 +52,25 @@ func (s *Sever) initResources() {
mcp.Required(),
),
), resourcesCreateOrUpdate)
s.server.AddTool(mcp.NewTool(
"resources_delete",
mcp.WithDescription("Delete a Kubernetes resource in the current cluster by providing its apiVersion, kind, optionally the namespace, and its name"),
mcp.WithString("apiVersion",
mcp.Description("apiVersion of the resource (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)"),
mcp.Required(),
),
mcp.WithString("kind",
mcp.Description("kind of the resource (examples of valid kind are: Pod, Service, Deployment, Ingress)"),
mcp.Required(),
),
mcp.WithString("namespace",
mcp.Description("Optional Namespace to delete the namespaced resource from (ignored in case of cluster scoped resources). If not provided, will delete resource from configured namespace"),
),
mcp.WithString("name",
mcp.Description("Name of the resource"),
mcp.Required(),
),
), resourcesDelete)
}
func resourcesList(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
@@ -114,6 +133,30 @@ func resourcesCreateOrUpdate(ctx context.Context, ctr mcp.CallToolRequest) (*mcp
return NewTextResult(ret, err), nil
}
func resourcesDelete(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
k, err := kubernetes.NewKubernetes()
if err != nil {
return NewTextResult("", fmt.Errorf("failed to delete resource: %v", err)), nil
}
namespace := ctr.Params.Arguments["namespace"]
if namespace == nil {
namespace = ""
}
gvk, err := parseGroupVersionKind(ctr.Params.Arguments)
if err != nil {
return NewTextResult("", fmt.Errorf("failed to delete resource, %s", err)), nil
}
name := ctr.Params.Arguments["name"]
if name == nil {
return NewTextResult("", errors.New("failed to delete resource, missing argument name")), nil
}
err = k.ResourcesDelete(ctx, gvk, namespace.(string), name.(string))
if err != nil {
return NewTextResult("", fmt.Errorf("failed to delete resource: %v", err)), nil
}
return NewTextResult("Resource deleted successfully", err), nil
}
func parseGroupVersionKind(arguments map[string]interface{}) (*schema.GroupVersionKind, error) {
apiVersion := arguments["apiVersion"]
if apiVersion == nil {

View File

@@ -322,3 +322,109 @@ func TestResourcesCreateOrUpdate(t *testing.T) {
})
})
}
func TestResourcesDelete(t *testing.T) {
testCase(t, func(c *mcpContext) {
c.withEnvTest()
t.Run("resources_delete with missing apiVersion returns error", func(t *testing.T) {
toolResult, _ := c.callTool("resources_delete", map[string]interface{}{})
if !toolResult.IsError {
t.Fatalf("call tool should fail")
return
}
if toolResult.Content[0].(map[string]interface{})["text"].(string) != "failed to delete resource, missing argument apiVersion" {
t.Fatalf("invalid error message, got %v", toolResult.Content[0].(map[string]interface{})["text"].(string))
return
}
})
t.Run("resources_delete with missing kind returns error", func(t *testing.T) {
toolResult, _ := c.callTool("resources_delete", map[string]interface{}{"apiVersion": "v1"})
if !toolResult.IsError {
t.Fatalf("call tool should fail")
return
}
if toolResult.Content[0].(map[string]interface{})["text"].(string) != "failed to delete resource, missing argument kind" {
t.Fatalf("invalid error message, got %v", toolResult.Content[0].(map[string]interface{})["text"].(string))
return
}
})
t.Run("resources_delete with invalid apiVersion returns error", func(t *testing.T) {
toolResult, _ := c.callTool("resources_delete", map[string]interface{}{"apiVersion": "invalid/api/version", "kind": "Pod", "name": "a-pod"})
if !toolResult.IsError {
t.Fatalf("call tool should fail")
return
}
if toolResult.Content[0].(map[string]interface{})["text"].(string) != "failed to delete resource, invalid argument apiVersion" {
t.Fatalf("invalid error message, got %v", toolResult.Content[0].(map[string]interface{})["text"].(string))
return
}
})
t.Run("resources_delete with nonexistent apiVersion returns error", func(t *testing.T) {
toolResult, _ := c.callTool("resources_delete", map[string]interface{}{"apiVersion": "custom.non.existent.example.com/v1", "kind": "Custom", "name": "a-custom"})
if !toolResult.IsError {
t.Fatalf("call tool should fail")
return
}
if toolResult.Content[0].(map[string]interface{})["text"].(string) != `failed to delete resource: no matches for kind "Custom" in version "custom.non.existent.example.com/v1"` {
t.Fatalf("invalid error message, got %v", toolResult.Content[0].(map[string]interface{})["text"].(string))
return
}
})
t.Run("resources_delete with missing name returns error", func(t *testing.T) {
toolResult, _ := c.callTool("resources_delete", map[string]interface{}{"apiVersion": "v1", "kind": "Namespace"})
if !toolResult.IsError {
t.Fatalf("call tool should fail")
return
}
if toolResult.Content[0].(map[string]interface{})["text"].(string) != "failed to delete resource, missing argument name" {
t.Fatalf("invalid error message, got %v", toolResult.Content[0].(map[string]interface{})["text"].(string))
return
}
})
resourcesDeleteCm, err := c.callTool("resources_delete", map[string]interface{}{"apiVersion": "v1", "kind": "ConfigMap", "name": "a-configmap-to-delete"})
t.Run("resources_delete with valid namespaced resource returns success", func(t *testing.T) {
if err != nil {
t.Fatalf("call tool failed %v", err)
return
}
if resourcesDeleteCm.IsError {
t.Fatalf("call tool failed")
return
}
if resourcesDeleteCm.Content[0].(map[string]interface{})["text"].(string) != "Resource deleted successfully" {
t.Fatalf("invalid tool result content got: %v", resourcesDeleteCm.Content[0].(map[string]interface{})["text"].(string))
return
}
})
client := c.newKubernetesClient()
t.Run("resources_delete with valid namespaced resource deletes ConfigMap", func(t *testing.T) {
_, err := client.CoreV1().ConfigMaps("default").Get(c.ctx, "a-configmap-to-delete", metav1.GetOptions{})
if err == nil {
t.Fatalf("ConfigMap not deleted")
return
}
})
resourcesDeleteNamespace, err := c.callTool("resources_delete", map[string]interface{}{"apiVersion": "v1", "kind": "Namespace", "name": "ns-to-delete"})
t.Run("resources_delete with valid namespaced resource returns success", func(t *testing.T) {
if err != nil {
t.Fatalf("call tool failed %v", err)
return
}
if resourcesDeleteNamespace.IsError {
t.Fatalf("call tool failed")
return
}
if resourcesDeleteNamespace.Content[0].(map[string]interface{})["text"].(string) != "Resource deleted successfully" {
t.Fatalf("invalid tool result content got: %v", resourcesDeleteNamespace.Content[0].(map[string]interface{})["text"].(string))
return
}
})
t.Run("resources_delete with valid namespaced resource deletes Namespace", func(t *testing.T) {
ns, err := client.CoreV1().Namespaces().Get(c.ctx, "ns-to-delete", metav1.GetOptions{})
if err == nil && ns != nil && ns.ObjectMeta.DeletionTimestamp == nil {
t.Fatalf("Namespace not deleted")
return
}
})
})
}