mirror of
https://github.com/openshift/openshift-mcp-server.git
synced 2025-10-17 14:27:48 +03:00
feat(kubernetes): resources_delete can get any resource in the cluster
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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{})
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user