Skip to content

Commit 256dbef

Browse files
committed
feat(kubevirt): Add vm_pause tool
Implements a new KubeVirt VM pause tool that uses the subresource API to pause a running VirtualMachine's associated VirtualMachineInstance. Unlike vm_start and vm_stop which modify the VM's runStrategy, this tool directly calls the VMI pause subresource endpoint at /apis/subresources.kubevirt.io/v1/namespaces/{namespace}/virtualmachineinstances/{name}/pause. Assisted-By: Claude <[email protected]> Signed-off-by: Lee Yarwood <[email protected]>
1 parent 4b7bdd6 commit 256dbef

File tree

3 files changed

+213
-0
lines changed

3 files changed

+213
-0
lines changed

pkg/toolsets/kubevirt/toolset.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
88
"github.com/containers/kubernetes-mcp-server/pkg/toolsets"
99
vm_create "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt/vm/create"
10+
vm_pause "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt/vm/pause"
1011
vm_start "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt/vm/start"
1112
vm_stop "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt/vm/stop"
1213
vm_troubleshoot "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt/vm/troubleshoot"
@@ -27,6 +28,7 @@ func (t *Toolset) GetDescription() string {
2728
func (t *Toolset) GetTools(o internalk8s.Openshift) []api.ServerTool {
2829
return slices.Concat(
2930
vm_create.Tools(),
31+
vm_pause.Tools(),
3032
vm_start.Tools(),
3133
vm_stop.Tools(),
3234
vm_troubleshoot.Tools(),
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package pause
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/containers/kubernetes-mcp-server/pkg/api"
7+
"github.com/google/jsonschema-go/jsonschema"
8+
"k8s.io/apimachinery/pkg/runtime"
9+
"k8s.io/apimachinery/pkg/runtime/schema"
10+
"k8s.io/apimachinery/pkg/runtime/serializer"
11+
"k8s.io/client-go/rest"
12+
"k8s.io/utils/ptr"
13+
)
14+
15+
var (
16+
// SchemeGroupVersion is the group version used for KubeVirt subresources
17+
SchemeGroupVersion = schema.GroupVersion{Group: "subresources.kubevirt.io", Version: "v1"}
18+
19+
// Scheme is the runtime scheme for KubeVirt subresources
20+
Scheme = runtime.NewScheme()
21+
22+
// Codecs provides access to encoding and decoding for the scheme
23+
Codecs = serializer.NewCodecFactory(Scheme)
24+
)
25+
26+
func Tools() []api.ServerTool {
27+
return []api.ServerTool{
28+
{
29+
Tool: api.Tool{
30+
Name: "vm_pause",
31+
Description: "Pause a running VirtualMachine by pausing its associated VirtualMachineInstance using the KubeVirt pause subresource API",
32+
InputSchema: &jsonschema.Schema{
33+
Type: "object",
34+
Properties: map[string]*jsonschema.Schema{
35+
"namespace": {
36+
Type: "string",
37+
Description: "The namespace of the virtual machine",
38+
},
39+
"name": {
40+
Type: "string",
41+
Description: "The name of the virtual machine to pause",
42+
},
43+
},
44+
Required: []string{"namespace", "name"},
45+
},
46+
Annotations: api.ToolAnnotations{
47+
Title: "Virtual Machine: Pause",
48+
ReadOnlyHint: ptr.To(false),
49+
DestructiveHint: ptr.To(false),
50+
IdempotentHint: ptr.To(true),
51+
OpenWorldHint: ptr.To(false),
52+
},
53+
},
54+
Handler: pause,
55+
},
56+
}
57+
}
58+
59+
func pause(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
60+
// Parse required parameters
61+
namespace, err := params.GetRequiredString("namespace")
62+
if err != nil {
63+
return api.NewToolCallResult("", err), nil
64+
}
65+
66+
name, err := params.GetRequiredString("name")
67+
if err != nil {
68+
return api.NewToolCallResult("", err), nil
69+
}
70+
71+
// Get REST config
72+
restConfig := params.RESTConfig()
73+
if restConfig == nil {
74+
return api.NewToolCallResult("", fmt.Errorf("failed to get REST config")), nil
75+
}
76+
77+
// Create a REST client for the KubeVirt subresource API
78+
restConfig.GroupVersion = &SchemeGroupVersion
79+
restConfig.APIPath = "/apis"
80+
restConfig.NegotiatedSerializer = Codecs.WithoutConversion()
81+
82+
restClient, err := rest.RESTClientFor(restConfig)
83+
if err != nil {
84+
return api.NewToolCallResult("", fmt.Errorf("failed to create REST client: %w", err)), nil
85+
}
86+
87+
// Call the pause subresource on the VMI
88+
// PUT /apis/subresources.kubevirt.io/v1/namespaces/{namespace}/virtualmachineinstances/{name}/pause
89+
result := restClient.Put().
90+
Namespace(namespace).
91+
Resource("virtualmachineinstances").
92+
Name(name).
93+
SubResource("pause").
94+
Do(params.Context)
95+
96+
if err := result.Error(); err != nil {
97+
return api.NewToolCallResult("", fmt.Errorf("failed to pause VirtualMachineInstance: %w", err)), nil
98+
}
99+
100+
return api.NewToolCallResult(fmt.Sprintf("VirtualMachineInstance %s/%s paused successfully", namespace, name), nil), nil
101+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package pause_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/containers/kubernetes-mcp-server/pkg/api"
8+
internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
9+
"github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt/vm/pause"
10+
)
11+
12+
type mockToolCallRequest struct {
13+
arguments map[string]interface{}
14+
}
15+
16+
func (m *mockToolCallRequest) GetArguments() map[string]any {
17+
return m.arguments
18+
}
19+
20+
func TestPauseParameterValidation(t *testing.T) {
21+
tests := []struct {
22+
name string
23+
args map[string]interface{}
24+
wantErr bool
25+
errMsg string
26+
}{
27+
{
28+
name: "missing namespace parameter",
29+
args: map[string]interface{}{
30+
"name": "test-vm",
31+
},
32+
wantErr: true,
33+
errMsg: "namespace parameter required",
34+
},
35+
{
36+
name: "missing name parameter",
37+
args: map[string]interface{}{
38+
"namespace": "test-ns",
39+
},
40+
wantErr: true,
41+
errMsg: "name parameter required",
42+
},
43+
{
44+
name: "invalid namespace type",
45+
args: map[string]interface{}{
46+
"namespace": 123,
47+
"name": "test-vm",
48+
},
49+
wantErr: true,
50+
errMsg: "namespace parameter must be a string",
51+
},
52+
{
53+
name: "invalid name type",
54+
args: map[string]interface{}{
55+
"namespace": "test-ns",
56+
"name": 456,
57+
},
58+
wantErr: true,
59+
errMsg: "name parameter must be a string",
60+
},
61+
{
62+
name: "valid parameters - cluster interaction expected",
63+
args: map[string]interface{}{
64+
"namespace": "test-ns",
65+
"name": "test-vm",
66+
},
67+
wantErr: true, // Will fail due to missing cluster connection, but parameters are valid
68+
},
69+
}
70+
71+
// Get the tool through the public API
72+
tools := pause.Tools()
73+
if len(tools) != 1 {
74+
t.Fatalf("Expected 1 tool, got %d", len(tools))
75+
}
76+
vmPauseTool := tools[0]
77+
78+
for _, tt := range tests {
79+
t.Run(tt.name, func(t *testing.T) {
80+
params := api.ToolHandlerParams{
81+
Context: context.Background(),
82+
Kubernetes: &internalk8s.Kubernetes{},
83+
ToolCallRequest: &mockToolCallRequest{arguments: tt.args},
84+
}
85+
86+
// Call through the public Handler interface
87+
result, err := vmPauseTool.Handler(params)
88+
if err != nil {
89+
t.Errorf("Handler() unexpected Go error: %v", err)
90+
return
91+
}
92+
93+
if result == nil {
94+
t.Error("Expected non-nil result")
95+
return
96+
}
97+
98+
// For parameter validation errors, check the error message
99+
if tt.wantErr && tt.errMsg != "" {
100+
if result.Error == nil {
101+
t.Error("Expected error in result.Error, got nil")
102+
return
103+
}
104+
if result.Error.Error() != tt.errMsg {
105+
t.Errorf("Expected error message %q, got %q", tt.errMsg, result.Error.Error())
106+
}
107+
}
108+
})
109+
}
110+
}

0 commit comments

Comments
 (0)