feat(kubernetes): pods_delete deletes managed resources

This commit is contained in:
Marc Nuri
2025-02-19 16:20:50 +01:00
parent 838e5863fa
commit 40ff50e04d
5 changed files with 297 additions and 30 deletions

View File

@@ -6,10 +6,12 @@ import (
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
labelutil "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/rand"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
)
@@ -32,8 +34,53 @@ func (k *Kubernetes) PodsGet(ctx context.Context, namespace, name string) (strin
}
func (k *Kubernetes) PodsDelete(ctx context.Context, namespace, name string) (string, error) {
// TODO
return "", nil
cs, err := kubernetes.NewForConfig(k.cfg)
if err != nil {
return "", err
}
namespace = namespaceOrDefault(namespace)
pod, err := cs.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return "", err
}
isManaged := pod.GetLabels()[AppKubernetesManagedBy] == version.BinaryName
managedLabelSelector := labelutil.Set{
AppKubernetesManagedBy: version.BinaryName,
AppKubernetesName: pod.GetLabels()[AppKubernetesName],
}.AsSelector()
// Delete managed service
if isManaged {
if sl, _ := cs.CoreV1().Services(namespace).List(ctx, metav1.ListOptions{
LabelSelector: managedLabelSelector.String(),
}); sl != nil {
for _, svc := range sl.Items {
_ = cs.CoreV1().Services(namespace).Delete(ctx, svc.Name, metav1.DeleteOptions{})
}
}
}
// Delete managed Route
if isManaged && k.supportsGroupVersion("route.openshift.io/v1") {
dynamicClient, dErr := dynamic.NewForConfig(k.cfg)
if dErr != nil {
return "", dErr
}
routeResources := dynamicClient.
Resource(schema.GroupVersionResource{Group: "route.openshift.io", Version: "v1", Resource: "routes"}).
Namespace(namespace)
if rl, _ := routeResources.List(ctx, metav1.ListOptions{
LabelSelector: managedLabelSelector.String(),
}); rl != nil {
for _, route := range rl.Items {
_ = routeResources.Delete(ctx, route.GetName(), metav1.DeleteOptions{})
}
}
}
return "Pod deleted successfully", cs.CoreV1().Pods(namespace).Delete(ctx, name, metav1.DeleteOptions{})
}
func (k *Kubernetes) PodsLog(ctx context.Context, namespace, name string) (string, error) {

View File

@@ -231,33 +231,18 @@ func createTestData(ctx context.Context, kc *kubernetes.Clientset) {
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"},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "nginx", Image: "nginx"},
},
},
}, metav1.CreateOptions{})
_, _ = kc.CoreV1().Pods("ns-1").
Create(ctx, &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "a-pod-in-ns-1"},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "nginx", Image: "nginx"},
},
},
}, metav1.CreateOptions{})
_, _ = kc.CoreV1().Pods("ns-2").
Create(ctx, &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "a-pod-in-ns-2"},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "nginx", Image: "nginx"},
},
},
}, metav1.CreateOptions{})
_, _ = kc.CoreV1().Pods("default").Create(ctx, &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "a-pod-in-default"},
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}},
}, metav1.CreateOptions{})
_, _ = kc.CoreV1().Pods("ns-1").Create(ctx, &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "a-pod-in-ns-1"},
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}},
}, metav1.CreateOptions{})
_, _ = kc.CoreV1().Pods("ns-2").Create(ctx, &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "a-pod-in-ns-2"},
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}},
}, metav1.CreateOptions{})
_, _ = kc.CoreV1().ConfigMaps("default").
Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "a-configmap-to-delete"}}, metav1.CreateOptions{})
}

View File

@@ -11,6 +11,7 @@ func TestTools(t *testing.T) {
"pods_list",
"pods_list_in_namespace",
"pods_get",
"pods_delete",
"pods_log",
"pods_run",
"resources_list",

View File

@@ -32,6 +32,17 @@ func (s *Sever) initPods() {
mcp.Required(),
),
), podsGet)
s.server.AddTool(mcp.NewTool(
"pods_delete",
mcp.WithDescription("Delete a Kubernetes Pod in the current or provided namespace with the provided name"),
mcp.WithString("namespace",
mcp.Description("Namespace to delete the Pod from"),
),
mcp.WithString("name",
mcp.Description("Name of the Pod to delete"),
mcp.Required(),
),
), podsDelete)
s.server.AddTool(mcp.NewTool(
"pods_log",
mcp.WithDescription("Get the logs of a Kubernetes Pod in the current or provided namespace with the provided name"),
@@ -39,7 +50,7 @@ func (s *Sever) initPods() {
mcp.Description("Namespace to get the Pod logs from"),
),
mcp.WithString("name",
mcp.Description("Name of the Pod"),
mcp.Description("Name of the Pod to get the logs from"),
mcp.Required(),
),
), podsLog)
@@ -110,6 +121,26 @@ func podsGet(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult,
return NewTextResult(ret, err), nil
}
func podsDelete(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
k, err := kubernetes.NewKubernetes()
if err != nil {
return NewTextResult("", fmt.Errorf("failed to delete pod: %v", err)), nil
}
ns := ctr.Params.Arguments["namespace"]
if ns == nil {
ns = ""
}
name := ctr.Params.Arguments["name"]
if name == nil {
return NewTextResult("", errors.New("failed to delete pod, missing argument name")), nil
}
ret, err := k.PodsDelete(ctx, ns.(string), name.(string))
if err != nil {
return NewTextResult("", fmt.Errorf("failed to delete pod %s in namespace %s: %v", name, ns, err)), nil
}
return NewTextResult(ret, err), nil
}
func podsLog(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
k, err := kubernetes.NewKubernetes()
if err != nil {

View File

@@ -1,7 +1,11 @@
package mcp
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"sigs.k8s.io/yaml"
"strings"
"testing"
@@ -134,6 +138,17 @@ func TestPodsGet(t *testing.T) {
return
}
})
t.Run("pods_get with not found name returns error", func(t *testing.T) {
toolResult, _ := c.callTool("pods_get", map[string]interface{}{"name": "not-found"})
if toolResult.IsError != true {
t.Fatalf("call tool should fail")
return
}
if toolResult.Content[0].(map[string]interface{})["text"].(string) != "failed to get pod not-found in namespace : pods \"not-found\" not found" {
t.Fatalf("invalid error message, got %v", toolResult.Content[0].(map[string]interface{})["text"].(string))
return
}
})
podsGetNilNamespace, err := c.callTool("pods_get", map[string]interface{}{
"name": "a-pod-in-default",
})
@@ -200,6 +215,194 @@ func TestPodsGet(t *testing.T) {
})
}
func TestPodsDelete(t *testing.T) {
testCase(t, func(c *mcpContext) {
c.withEnvTest()
// Errors
t.Run("pods_delete with nil name returns error", func(t *testing.T) {
toolResult, _ := c.callTool("pods_delete", map[string]interface{}{})
if toolResult.IsError != true {
t.Errorf("call tool should fail")
return
}
if toolResult.Content[0].(map[string]interface{})["text"].(string) != "failed to delete pod, missing argument name" {
t.Errorf("invalid error message, got %v", toolResult.Content[0].(map[string]interface{})["text"].(string))
return
}
})
t.Run("pods_delete with not found name returns error", func(t *testing.T) {
toolResult, _ := c.callTool("pods_delete", map[string]interface{}{"name": "not-found"})
if toolResult.IsError != true {
t.Errorf("call tool should fail")
return
}
if toolResult.Content[0].(map[string]interface{})["text"].(string) != "failed to delete pod not-found in namespace : pods \"not-found\" not found" {
t.Errorf("invalid error message, got %v", toolResult.Content[0].(map[string]interface{})["text"].(string))
return
}
})
// Default/nil Namespace
kc := c.newKubernetesClient()
_, _ = kc.CoreV1().Pods("default").Create(c.ctx, &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "a-pod-to-delete"},
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}},
}, metav1.CreateOptions{})
podsDeleteNilNamespace, err := c.callTool("pods_delete", map[string]interface{}{
"name": "a-pod-to-delete",
})
t.Run("pods_delete with name and nil namespace returns success", func(t *testing.T) {
if err != nil {
t.Errorf("call tool failed %v", err)
return
}
if podsDeleteNilNamespace.IsError {
t.Errorf("call tool failed")
return
}
if podsDeleteNilNamespace.Content[0].(map[string]interface{})["text"].(string) != "Pod deleted successfully" {
t.Errorf("invalid tool result content, got %v", podsDeleteNilNamespace.Content[0].(map[string]interface{})["text"].(string))
return
}
})
t.Run("pods_delete with name and nil namespace deletes Pod", func(t *testing.T) {
p, pErr := kc.CoreV1().Pods("default").Get(c.ctx, "a-pod-to-delete", metav1.GetOptions{})
if pErr == nil && p != nil && p.ObjectMeta.DeletionTimestamp == nil {
t.Errorf("Pod not deleted")
return
}
})
// Provided Namespace
_, _ = kc.CoreV1().Pods("ns-1").Create(c.ctx, &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "a-pod-to-delete-in-ns-1"},
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}},
}, metav1.CreateOptions{})
podsDeleteInNamespace, err := c.callTool("pods_delete", map[string]interface{}{
"namespace": "ns-1",
"name": "a-pod-to-delete-in-ns-1",
})
t.Run("pods_delete with name and namespace returns success", func(t *testing.T) {
if err != nil {
t.Errorf("call tool failed %v", err)
return
}
if podsDeleteInNamespace.IsError {
t.Errorf("call tool failed")
return
}
if podsDeleteInNamespace.Content[0].(map[string]interface{})["text"].(string) != "Pod deleted successfully" {
t.Errorf("invalid tool result content, got %v", podsDeleteInNamespace.Content[0].(map[string]interface{})["text"].(string))
return
}
})
t.Run("pods_delete with name and namespace deletes Pod", func(t *testing.T) {
p, pErr := kc.CoreV1().Pods("ns-1").Get(c.ctx, "a-pod-to-delete-in-ns-1", metav1.GetOptions{})
if pErr == nil && p != nil && p.ObjectMeta.DeletionTimestamp == nil {
t.Errorf("Pod not deleted")
return
}
})
// Managed Pod
managedLabels := map[string]string{
"app.kubernetes.io/managed-by": "kubernetes-mcp-server",
"app.kubernetes.io/name": "a-manged-pod-to-delete",
}
_, _ = kc.CoreV1().Pods("default").Create(c.ctx, &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "a-managed-pod-to-delete", Labels: managedLabels},
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}},
}, metav1.CreateOptions{})
_, _ = kc.CoreV1().Services("default").Create(c.ctx, &corev1.Service{
ObjectMeta: metav1.ObjectMeta{Name: "a-managed-service-to-delete", Labels: managedLabels},
Spec: corev1.ServiceSpec{Selector: managedLabels, Ports: []corev1.ServicePort{{Port: 80}}},
}, metav1.CreateOptions{})
podsDeleteManaged, err := c.callTool("pods_delete", map[string]interface{}{
"name": "a-managed-pod-to-delete",
})
t.Run("pods_delete with managed pod returns success", func(t *testing.T) {
if err != nil {
t.Errorf("call tool failed %v", err)
return
}
if podsDeleteManaged.IsError {
t.Errorf("call tool failed")
return
}
if podsDeleteManaged.Content[0].(map[string]interface{})["text"].(string) != "Pod deleted successfully" {
t.Errorf("invalid tool result content, got %v", podsDeleteManaged.Content[0].(map[string]interface{})["text"].(string))
return
}
})
t.Run("pods_delete with managed pod deletes Pod and Service", func(t *testing.T) {
p, pErr := kc.CoreV1().Pods("default").Get(c.ctx, "a-managed-pod-to-delete", metav1.GetOptions{})
if pErr == nil && p != nil && p.ObjectMeta.DeletionTimestamp == nil {
t.Errorf("Pod not deleted")
return
}
s, sErr := kc.CoreV1().Services("default").Get(c.ctx, "a-managed-service-to-delete", metav1.GetOptions{})
if sErr == nil && s != nil && s.ObjectMeta.DeletionTimestamp == nil {
t.Errorf("Service not deleted")
return
}
})
})
}
func TestPodsDeleteInOpenShift(t *testing.T) {
testCase(t, func(c *mcpContext) {
// Managed Pod in OpenShift
defer c.inOpenShift()() // n.b. two sets of parentheses to invoke the first function
managedLabels := map[string]string{
"app.kubernetes.io/managed-by": "kubernetes-mcp-server",
"app.kubernetes.io/name": "a-manged-pod-to-delete",
}
kc := c.newKubernetesClient()
_, _ = kc.CoreV1().Pods("default").Create(c.ctx, &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "a-managed-pod-to-delete-in-openshift", Labels: managedLabels},
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}},
}, metav1.CreateOptions{})
dynamicClient := dynamic.NewForConfigOrDie(envTestRestConfig)
_, _ = dynamicClient.Resource(schema.GroupVersionResource{Group: "route.openshift.io", Version: "v1", Resource: "routes"}).
Namespace("default").Create(c.ctx, &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "route.openshift.io/v1",
"kind": "Route",
"metadata": map[string]interface{}{
"name": "a-managed-route-to-delete",
"labels": managedLabels,
},
}}, metav1.CreateOptions{})
podsDeleteManagedOpenShift, err := c.callTool("pods_delete", map[string]interface{}{
"name": "a-managed-pod-to-delete-in-openshift",
})
t.Run("pods_delete with managed pod in OpenShift returns success", func(t *testing.T) {
if err != nil {
t.Errorf("call tool failed %v", err)
return
}
if podsDeleteManagedOpenShift.IsError {
t.Errorf("call tool failed")
return
}
if podsDeleteManagedOpenShift.Content[0].(map[string]interface{})["text"].(string) != "Pod deleted successfully" {
t.Errorf("invalid tool result content, got %v", podsDeleteManagedOpenShift.Content[0].(map[string]interface{})["text"].(string))
return
}
})
t.Run("pods_delete with managed pod in OpenShift deletes Pod and Route", func(t *testing.T) {
p, pErr := kc.CoreV1().Pods("default").Get(c.ctx, "a-managed-pod-to-delete-in-openshift", metav1.GetOptions{})
if pErr == nil && p != nil && p.ObjectMeta.DeletionTimestamp == nil {
t.Errorf("Pod not deleted")
return
}
r, rErr := dynamicClient.
Resource(schema.GroupVersionResource{Group: "route.openshift.io", Version: "v1", Resource: "routes"}).
Namespace("default").Get(c.ctx, "a-managed-route-to-delete", metav1.GetOptions{})
if rErr == nil && r != nil && r.GetDeletionTimestamp() == nil {
t.Errorf("Route not deleted")
return
}
})
})
}
func TestPodsLog(t *testing.T) {
testCase(t, func(c *mcpContext) {
c.withEnvTest()