Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -509,9 +509,14 @@ In case multi-cluster support is enabled (default) and you have access to multip
- `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)
- `action` (`string`) **(required)** - The lifecycle action to perform: 'start', 'stop', or 'restart'
- `name` (`string`) **(required)** - The name of the virtual machine
- `namespace` (`string`) **(required)** - The namespace of the virtual machine
- `run_policy` (`string`) - The run policy to use when starting a VM (only applies to 'start' action). Options:
- 'HighAvailability': VM runs continuously (sets runStrategy to Always)
- 'RestartOnFailure': VM restarts on failure (sets runStrategy to RerunOnFailure)
- 'Once': VM runs once and stops after completion (sets runStrategy to Once)
Defaults to 'HighAvailability' if not specified.

</details>

Expand Down
50 changes: 42 additions & 8 deletions pkg/kubevirt/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,19 @@ import (

// RunStrategy represents the run strategy for a VirtualMachine
type RunStrategy string
type RunPolicy string

const (
RunStrategyAlways RunStrategy = "Always"
RunStrategyHalted RunStrategy = "Halted"
RunStrategyAlways RunStrategy = "Always"
RunStrategyHalted RunStrategy = "Halted"
RunStrategyManual RunStrategy = "Manual"
RunStrategyRerunOnFailure RunStrategy = "RerunOnFailure"
RunStrategyOnce RunStrategy = "Once"
RunStrategyWaitAsReceiver RunStrategy = "WaitAsReceiver"

RunPolicyHighAvailability RunPolicy = "HighAvailability"
RunPolicyRestartOnFailure RunPolicy = "RestartOnFailure"
RunPolicyOnce RunPolicy = "Once"
)

// GetVirtualMachine retrieves a VirtualMachine by namespace and name
Expand Down Expand Up @@ -45,9 +54,17 @@ func UpdateVirtualMachine(ctx context.Context, client dynamic.Interface, vm *uns
Update(ctx, vm, metav1.UpdateOptions{})
}

// 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) {
// StartVM starts a VirtualMachine by updating its runStrategy based on the runPolicy
// runPolicy can be one of: HighAvailability, RestartOnFailure, Once
// - HighAvailability: The VM will be started if it is not already running, if it is already running the runStrategy
// will be set to Always.
// - RestartOnFailure: The VM will be started if it is not already running and will be restarted if it fails, if it
// is already running the runStrategy will be set to RerunOnFailure.
// - Once: The VM will be started if it is not already running and will be stopped after it completes, if it is already
// running the runStrategy will be set to Once.
// Returns the updated VM and true if the VM was started, or if it was already running and the runStrategy changed.
// Returns false if it was already running and the runStrategy did not change.
func StartVM(ctx context.Context, dynamicClient dynamic.Interface, namespace, name string, runPolicy RunPolicy) (*unstructured.Unstructured, bool, error) {
// Get the current VirtualMachine
vm, err := GetVirtualMachine(ctx, dynamicClient, namespace, name)
if err != nil {
Expand All @@ -60,12 +77,12 @@ func StartVM(ctx context.Context, dynamicClient dynamic.Interface, namespace, na
}

// Check if already running
if found && currentStrategy == RunStrategyAlways {
if found && currentStrategy == getRunStrategyFromRunPolicy(runPolicy) {
return vm, false, nil
}

// Update runStrategy to Always
if err := SetVMRunStrategy(vm, RunStrategyAlways); err != nil {
// Update runStrategy to the appropriate value
if err := SetVMRunStrategy(vm, getRunStrategyFromRunPolicy(runPolicy)); err != nil {
return nil, false, fmt.Errorf("failed to set runStrategy: %w", err)
}

Expand All @@ -78,6 +95,23 @@ func StartVM(ctx context.Context, dynamicClient dynamic.Interface, namespace, na
return updatedVM, true, nil
}

// getRunStrategyFromRunPolicy returns the RunStrategy for a given RunPolicy
// - HighAvailability: Always
// - RestartOnFailure: RerunOnFailure
// - Once: Once
// Returns the RunStrategy
func getRunStrategyFromRunPolicy(runPolicy RunPolicy) RunStrategy {
switch runPolicy {
case RunPolicyHighAvailability:
return RunStrategyAlways
case RunPolicyRestartOnFailure:
return RunStrategyRerunOnFailure
case RunPolicyOnce:
return RunStrategyOnce
}
return RunStrategyAlways
}

// StopVM stops a VirtualMachine by updating its runStrategy to Halted
// Returns the updated VM and true if the VM was stopped, false if it was already stopped
func StopVM(ctx context.Context, dynamicClient dynamic.Interface, namespace, name string) (*unstructured.Unstructured, bool, error) {
Expand Down
186 changes: 154 additions & 32 deletions pkg/kubevirt/vm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,39 +29,123 @@ func createTestVM(name, namespace string, runStrategy RunStrategy) *unstructured

func TestStartVM(t *testing.T) {
tests := []struct {
name string
initialVM *unstructured.Unstructured
wantStarted bool
wantError bool
errorContains string
name string
runPolicy RunPolicy
initialVM *unstructured.Unstructured
wantStarted bool
wantRunStrategy RunStrategy
wantError bool
errorContains string
}{
// HighAvailability policy tests
{
name: "Start VM that is Halted",
initialVM: createTestVM("test-vm", "default", RunStrategyHalted),
wantStarted: true,
wantError: false,
name: "HighAvailability: Start halted VM",
runPolicy: RunPolicyHighAvailability,
initialVM: createTestVM("test-vm", "default", RunStrategyHalted),
wantStarted: true,
wantRunStrategy: RunStrategyAlways,
},
{
name: "Start VM that is already running (Always)",
initialVM: createTestVM("test-vm", "default", RunStrategyAlways),
wantStarted: false,
wantError: false,
name: "HighAvailability: VM already running with Always",
runPolicy: RunPolicyHighAvailability,
initialVM: createTestVM("test-vm", "default", RunStrategyAlways),
wantStarted: false,
wantRunStrategy: RunStrategyAlways,
},
{
name: "Start VM without runStrategy",
initialVM: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "kubevirt.io/v1",
"kind": "VirtualMachine",
"metadata": map[string]interface{}{
"name": "test-vm",
"namespace": "default",
},
"spec": map[string]interface{}{},
},
},
wantStarted: true,
wantError: false,
name: "HighAvailability: Change from RerunOnFailure to Always",
runPolicy: RunPolicyHighAvailability,
initialVM: createTestVM("test-vm", "default", RunStrategyRerunOnFailure),
wantStarted: true,
wantRunStrategy: RunStrategyAlways,
},
{
name: "HighAvailability: Change from Once to Always",
runPolicy: RunPolicyHighAvailability,
initialVM: createTestVM("test-vm", "default", RunStrategyOnce),
wantStarted: true,
wantRunStrategy: RunStrategyAlways,
},
{
name: "HighAvailability: Change from Manual to Always",
runPolicy: RunPolicyHighAvailability,
initialVM: createTestVM("test-vm", "default", RunStrategyManual),
wantStarted: true,
wantRunStrategy: RunStrategyAlways,
},

// RestartOnFailure policy tests
{
name: "RestartOnFailure: Start halted VM",
runPolicy: RunPolicyRestartOnFailure,
initialVM: createTestVM("test-vm", "default", RunStrategyHalted),
wantStarted: true,
wantRunStrategy: RunStrategyRerunOnFailure,
},
{
name: "RestartOnFailure: VM already running with RerunOnFailure",
runPolicy: RunPolicyRestartOnFailure,
initialVM: createTestVM("test-vm", "default", RunStrategyRerunOnFailure),
wantStarted: false,
wantRunStrategy: RunStrategyRerunOnFailure,
},
{
name: "RestartOnFailure: Change from Always to RerunOnFailure",
runPolicy: RunPolicyRestartOnFailure,
initialVM: createTestVM("test-vm", "default", RunStrategyAlways),
wantStarted: true,
wantRunStrategy: RunStrategyRerunOnFailure,
},
{
name: "RestartOnFailure: Change from Once to RerunOnFailure",
runPolicy: RunPolicyRestartOnFailure,
initialVM: createTestVM("test-vm", "default", RunStrategyOnce),
wantStarted: true,
wantRunStrategy: RunStrategyRerunOnFailure,
},
{
name: "RestartOnFailure: Change from Manual to RerunOnFailure",
runPolicy: RunPolicyRestartOnFailure,
initialVM: createTestVM("test-vm", "default", RunStrategyManual),
wantStarted: true,
wantRunStrategy: RunStrategyRerunOnFailure,
},

// Once policy tests
{
name: "Once: Start halted VM",
runPolicy: RunPolicyOnce,
initialVM: createTestVM("test-vm", "default", RunStrategyHalted),
wantStarted: true,
wantRunStrategy: RunStrategyOnce,
},
{
name: "Once: VM already running with Once",
runPolicy: RunPolicyOnce,
initialVM: createTestVM("test-vm", "default", RunStrategyOnce),
wantStarted: false,
wantRunStrategy: RunStrategyOnce,
},
{
name: "Once: Change from Always to Once",
runPolicy: RunPolicyOnce,
initialVM: createTestVM("test-vm", "default", RunStrategyAlways),
wantStarted: true,
wantRunStrategy: RunStrategyOnce,
},
{
name: "Once: Change from RerunOnFailure to Once",
runPolicy: RunPolicyOnce,
initialVM: createTestVM("test-vm", "default", RunStrategyRerunOnFailure),
wantStarted: true,
wantRunStrategy: RunStrategyOnce,
},
{
name: "Once: Change from Manual to Once",
runPolicy: RunPolicyOnce,
initialVM: createTestVM("test-vm", "default", RunStrategyManual),
wantStarted: true,
wantRunStrategy: RunStrategyOnce,
},
}

Expand All @@ -71,7 +155,7 @@ func TestStartVM(t *testing.T) {
client := fake.NewSimpleDynamicClient(scheme, tt.initialVM)
ctx := context.Background()

vm, wasStarted, err := StartVM(ctx, client, tt.initialVM.GetNamespace(), tt.initialVM.GetName())
vm, wasStarted, err := StartVM(ctx, client, tt.initialVM.GetNamespace(), tt.initialVM.GetName(), tt.runPolicy)

if tt.wantError {
if err == nil {
Expand All @@ -98,7 +182,7 @@ func TestStartVM(t *testing.T) {
t.Errorf("wasStarted = %v, want %v", wasStarted, tt.wantStarted)
}

// Verify the VM's runStrategy is Always
// Verify the VM's runStrategy matches expected
strategy, found, err := GetVMRunStrategy(vm)
if err != nil {
t.Errorf("Failed to get runStrategy: %v", err)
Expand All @@ -108,8 +192,8 @@ func TestStartVM(t *testing.T) {
t.Errorf("runStrategy not found")
return
}
if strategy != RunStrategyAlways {
t.Errorf("Strategy = %q, want %q", strategy, RunStrategyAlways)
if strategy != tt.wantRunStrategy {
t.Errorf("Strategy = %q, want %q", strategy, tt.wantRunStrategy)
}
})
}
Expand All @@ -120,7 +204,7 @@ func TestStartVMNotFound(t *testing.T) {
client := fake.NewSimpleDynamicClient(scheme)
ctx := context.Background()

_, _, err := StartVM(ctx, client, "default", "non-existent-vm")
_, _, err := StartVM(ctx, client, "default", "non-existent-vm", RunPolicyHighAvailability)
if err == nil {
t.Errorf("Expected error for non-existent VM, got nil")
return
Expand Down Expand Up @@ -327,3 +411,41 @@ func TestRestartVMNotFound(t *testing.T) {
t.Errorf("Error = %v, want to contain 'failed to get VirtualMachine'", err)
}
}

func TestGetRunStrategyFromRunPolicy(t *testing.T) {
tests := []struct {
name string
runPolicy RunPolicy
wantRunStrategy RunStrategy
}{
{
name: "HighAvailability maps to Always",
runPolicy: RunPolicyHighAvailability,
wantRunStrategy: RunStrategyAlways,
},
{
name: "RestartOnFailure maps to RerunOnFailure",
runPolicy: RunPolicyRestartOnFailure,
wantRunStrategy: RunStrategyRerunOnFailure,
},
{
name: "Once maps to Once",
runPolicy: RunPolicyOnce,
wantRunStrategy: RunStrategyOnce,
},
{
name: "Invalid policy defaults to Always",
runPolicy: RunPolicy("invalid"),
wantRunStrategy: RunStrategyAlways,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := getRunStrategyFromRunPolicy(tt.runPolicy)
if got != tt.wantRunStrategy {
t.Errorf("getRunStrategyFromRunPolicy(%q) = %q, want %q", tt.runPolicy, got, tt.wantRunStrategy)
}
})
}
}
Loading