From f6561715fdc9ab175a82407e9400546ab2113c1d Mon Sep 17 00:00:00 2001 From: Ben Oukhanov Date: Wed, 10 Dec 2025 18:11:12 +0200 Subject: [PATCH] feat: add delete action in VM lifecycle Add a new action to delete the VM during the lifecycle of it. Signed-off-by: Ben Oukhanov --- README.md | 4 +- pkg/kubevirt/vm.go | 7 ++++ pkg/kubevirt/vm_test.go | 38 +++++++++++++++++++ pkg/mcp/kubevirt_test.go | 24 +++++++++++- pkg/mcp/testdata/toolsets-kubevirt-tools.json | 7 ++-- pkg/toolsets/kubevirt/vm/lifecycle/tool.go | 13 +++++-- 6 files changed, 84 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b62630941..00a9f7e09 100644 --- a/README.md +++ b/README.md @@ -524,8 +524,8 @@ In case multi-cluster support is enabled (default) and you have access to multip - `storage` (`string`) - Optional storage size for the VM's root disk when using DataSources (e.g., '30Gi', '50Gi', '100Gi'). Defaults to 30Gi. Ignored when using container disks. - `workload` (`string`) - The workload for the VM. Accepts OS names (e.g., 'fedora' (default), 'ubuntu', 'centos', 'centos-stream', 'debian', 'rhel', 'opensuse', 'opensuse-tumbleweed', 'opensuse-leap') or full container disk image URLs -- **vm_lifecycle** - Manage VirtualMachine lifecycle: start, stop, or restart a VM - - `action` (`string`) **(required)** - The lifecycle action to perform: 'start' (changes runStrategy to Always), 'stop' (changes runStrategy to Halted), or 'restart' (stops then starts the VM) +- **vm_lifecycle** - Manage VirtualMachine lifecycle: start, stop, restart, or delete a VM + - `action` (`string`) **(required)** - The lifecycle action to perform: 'start' (changes runStrategy to Always), 'stop' (changes runStrategy to Halted), or 'restart' (stops then starts the VM), or 'delete' (deletes the VM) - `name` (`string`) **(required)** - The name of the virtual machine - `namespace` (`string`) **(required)** - The namespace of the virtual machine diff --git a/pkg/kubevirt/vm.go b/pkg/kubevirt/vm.go index 0a381c8a9..7525e1c3d 100644 --- a/pkg/kubevirt/vm.go +++ b/pkg/kubevirt/vm.go @@ -62,6 +62,13 @@ func UpdateVirtualMachine(ctx context.Context, client dynamic.Interface, vm *uns Update(ctx, vm, metav1.UpdateOptions{}) } +// DeleteVirtualMachine deletes a VirtualMachine by namespace and name +func DeleteVirtualMachine(ctx context.Context, client dynamic.Interface, namespace, name string) error { + return client.Resource(VirtualMachineGVR). + Namespace(namespace). + Delete(ctx, name, metav1.DeleteOptions{}) +} + // StartVM starts a VirtualMachine by updating its runStrategy to Always // Returns the updated VM and true if the VM was started, false if it was already running func StartVM(ctx context.Context, dynamicClient dynamic.Interface, namespace, name string) (*unstructured.Unstructured, bool, error) { diff --git a/pkg/kubevirt/vm_test.go b/pkg/kubevirt/vm_test.go index 6afd4dbe2..f3bacf868 100644 --- a/pkg/kubevirt/vm_test.go +++ b/pkg/kubevirt/vm_test.go @@ -327,3 +327,41 @@ func TestRestartVMNotFound(t *testing.T) { t.Errorf("Error = %v, want to contain 'failed to get VirtualMachine'", err) } } + +func TestDeleteVM(t *testing.T) { + scheme := runtime.NewScheme() + initialVM := createTestVM("test-vm", "default", RunStrategyAlways) + client := fake.NewSimpleDynamicClient(scheme, initialVM) + ctx := context.Background() + + err := DeleteVirtualMachine(ctx, client, "default", "test-vm") + if err != nil { + t.Errorf("Unexpected error during deletion: %v", err) + return + } + + // Verify the VM is deleted + _, err = GetVirtualMachine(ctx, client, "default", "test-vm") + if err == nil { + t.Errorf("Expected error when getting deleted VM, got nil") + return + } + if !strings.Contains(err.Error(), "not found") { + t.Errorf("Error = %v, want to contain 'not found'", err) + } +} + +func TestDeleteVMNotFound(t *testing.T) { + scheme := runtime.NewScheme() + client := fake.NewSimpleDynamicClient(scheme) + ctx := context.Background() + + err := DeleteVirtualMachine(ctx, client, "default", "non-existent-vm") + if err == nil { + t.Errorf("Expected error for non-existent VM deletion, got nil") + return + } + if !strings.Contains(err.Error(), "not found") { + t.Errorf("Error = %v, want to contain 'not found'", err) + } +} diff --git a/pkg/mcp/kubevirt_test.go b/pkg/mcp/kubevirt_test.go index aa2514224..b5f118d55 100644 --- a/pkg/mcp/kubevirt_test.go +++ b/pkg/mcp/kubevirt_test.go @@ -651,8 +651,30 @@ func (s *KubevirtSuite) TestVMLifecycle() { }) }) + s.Run("vm_lifecycle action=delete on running VM", func() { + toolResult, err := s.CallTool("vm_lifecycle", map[string]interface{}{ + "name": "test-vm-lifecycle", + "namespace": "default", + "action": "delete", + }) + s.Run("no error", func() { + s.Nilf(err, "call tool failed %v", err) + s.Falsef(toolResult.IsError, "call tool failed") + }) + var decodedResult []unstructured.Unstructured + err = yaml.Unmarshal([]byte(toolResult.Content[0].(mcp.TextContent).Text), &decodedResult) + s.Run("returns yaml content", func() { + s.Nilf(err, "invalid tool result content %v", err) + s.Truef(strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "# VirtualMachine deleted successfully"), + "Expected success message, got %v", toolResult.Content[0].(mcp.TextContent).Text) + s.Require().Lenf(decodedResult, 1, "invalid resource count, expected 1, got %v", len(decodedResult)) + s.Equal("test-vm-lifecycle", decodedResult[0].GetName(), "invalid resource name") + s.Equal("default", decodedResult[0].GetNamespace(), "invalid resource namespace") + }) + }) + s.Run("vm_lifecycle on non-existent VM", func() { - for _, action := range []string{"start", "stop", "restart"} { + for _, action := range []string{"start", "stop", "restart", "delete"} { s.Run("action="+action, func() { toolResult, err := s.CallTool("vm_lifecycle", map[string]interface{}{ "name": "non-existent-vm", diff --git a/pkg/mcp/testdata/toolsets-kubevirt-tools.json b/pkg/mcp/testdata/toolsets-kubevirt-tools.json index 37167b77b..e4bc1f9a0 100644 --- a/pkg/mcp/testdata/toolsets-kubevirt-tools.json +++ b/pkg/mcp/testdata/toolsets-kubevirt-tools.json @@ -84,16 +84,17 @@ "destructiveHint": true, "openWorldHint": false }, - "description": "Manage VirtualMachine lifecycle: start, stop, or restart a VM", + "description": "Manage VirtualMachine lifecycle: start, stop, restart, or delete a VM", "inputSchema": { "type": "object", "properties": { "action": { - "description": "The lifecycle action to perform: 'start' (changes runStrategy to Always), 'stop' (changes runStrategy to Halted), or 'restart' (stops then starts the VM)", + "description": "The lifecycle action to perform: 'start' (changes runStrategy to Always), 'stop' (changes runStrategy to Halted), or 'restart' (stops then starts the VM), or 'delete' (deletes the VM)", "enum": [ "start", "stop", - "restart" + "restart", + "delete" ], "type": "string" }, diff --git a/pkg/toolsets/kubevirt/vm/lifecycle/tool.go b/pkg/toolsets/kubevirt/vm/lifecycle/tool.go index 517b0bcae..6ffd1bf9d 100644 --- a/pkg/toolsets/kubevirt/vm/lifecycle/tool.go +++ b/pkg/toolsets/kubevirt/vm/lifecycle/tool.go @@ -18,6 +18,7 @@ const ( ActionStart Action = "start" ActionStop Action = "stop" ActionRestart Action = "restart" + ActionDelete Action = "delete" ) func Tools() []api.ServerTool { @@ -25,7 +26,7 @@ func Tools() []api.ServerTool { { Tool: api.Tool{ Name: "vm_lifecycle", - Description: "Manage VirtualMachine lifecycle: start, stop, or restart a VM", + Description: "Manage VirtualMachine lifecycle: start, stop, restart, or delete a VM", InputSchema: &jsonschema.Schema{ Type: "object", Properties: map[string]*jsonschema.Schema{ @@ -39,8 +40,8 @@ func Tools() []api.ServerTool { }, "action": { Type: "string", - Enum: []any{string(ActionStart), string(ActionStop), string(ActionRestart)}, - Description: "The lifecycle action to perform: 'start' (changes runStrategy to Always), 'stop' (changes runStrategy to Halted), or 'restart' (stops then starts the VM)", + Enum: []any{string(ActionStart), string(ActionStop), string(ActionRestart), string(ActionDelete)}, + Description: "The lifecycle action to perform: 'start' (changes runStrategy to Always), 'stop' (changes runStrategy to Halted), 'restart' (stops then starts the VM), or 'delete' (deletes the VM)", }, }, Required: []string{"namespace", "name", "action"}, @@ -111,6 +112,12 @@ func lifecycle(params api.ToolHandlerParams) (*api.ToolCallResult, error) { return api.NewToolCallResult("", err), nil } message = "# VirtualMachine restarted successfully\n" + case ActionDelete: + err = kubevirt.DeleteVirtualMachine(params.Context, dynamicClient, namespace, name) + if err != nil { + return api.NewToolCallResult("", err), nil + } + message = "# VirtualMachine deleted successfully\n" default: return api.NewToolCallResult("", fmt.Errorf("invalid action '%s': must be one of 'start', 'stop', 'restart'", action)), nil