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
30 changes: 30 additions & 0 deletions api/hypershift/v1beta1/kubevirt.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,18 @@ const (
MultiQueueDisable MultiQueueSetting = "Disable"
)

// KubevirtEvictionStrategy defines the eviction behavior for KubeVirt VMs.
//
// +kubebuilder:validation:Enum=LiveMigrate;LiveMigrateIfPossible;External;None
type KubevirtEvictionStrategy string

const (
EvictionStrategyLiveMigrate KubevirtEvictionStrategy = "LiveMigrate"
EvictionStrategyLiveMigrateIfPossible KubevirtEvictionStrategy = "LiveMigrateIfPossible"
EvictionStrategyExternal KubevirtEvictionStrategy = "External"
EvictionStrategyNone KubevirtEvictionStrategy = "None"
)

// KubevirtNodePoolPlatform specifies the configuration of a NodePool when operating
// on KubeVirt platform.
type KubevirtNodePoolPlatform struct {
Expand Down Expand Up @@ -191,6 +203,24 @@ type KubevirtNodePoolPlatform struct {
// +optional
// +kubebuilder:validation:MaxItems=10
KubevirtHostDevices []KubevirtHostDevice `json:"hostDevices,omitempty"`

// evictionStrategy defines the eviction behavior for KubeVirt VMs during
// infrastructure node drain. If not set, the cluster-level default from the
// KubeVirt configuration applies (typically LiveMigrate on HA clusters when
// managed by HCO).
//
// - LiveMigrate: KubeVirt live-migrates the VM to another node (zero guest
// disruption). If the VM is not migratable, the node drain stalls.
// - LiveMigrateIfPossible: live-migrates if possible, otherwise allows the
// VMI pod to be killed (no graceful guest drain).
// - External: delegates eviction handling to CAPK, which drains the guest
// node before deleting the VMI. Use this for non-migratable VMs (e.g.,
// with GPU passthrough or SR-IOV) that need graceful guest node drain.
// - None: the VM is killed immediately on eviction with no migration or
// graceful guest drain. This is the HCO default on single-node OpenShift.
//
// +optional
EvictionStrategy KubevirtEvictionStrategy `json:"evictionStrategy,omitempty"`
}

// KubevirtNetwork specifies the configuration for a virtual machine
Expand Down
132 changes: 130 additions & 2 deletions api/hypershift/v1beta1/nodepool_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package v1beta1

import (
"encoding/json"
"reflect"
"testing"

"k8s.io/utils/ptr"
Expand All @@ -21,9 +22,9 @@ func TestNodePoolAutoScalingSerializationCompatibility(t *testing.T) {
name string
// current is the N (current) version of the struct
current NodePoolAutoScaling
// expectedJSON is the expected JSON output from marshalling current
// expectedJSON is the expected JSON output from marshaling current
expectedJSON string
// nMinus1Result is the expected result when unmarshalling into the N-1 struct
// nMinus1Result is the expected result when unmarshaling into the N-1 struct
nMinus1Result nodePoolAutoScalingNMinus1
}{
{
Expand Down Expand Up @@ -93,3 +94,130 @@ func TestNodePoolAutoScalingSerializationCompatibility(t *testing.T) {
})
}
}

func TestKubevirtEvictionStrategySerializationCompatibility(t *testing.T) {
tests := []struct {
name string
current KubevirtNodePoolPlatform
expectedJSON string
}{
{
name: "When EvictionStrategy is not set it should be omitted from JSON",
current: KubevirtNodePoolPlatform{
RootVolume: &KubevirtRootVolume{},
},
expectedJSON: `{"rootVolume":{}}`,
},
{
name: "When EvictionStrategy is External it should be preserved",
current: KubevirtNodePoolPlatform{
RootVolume: &KubevirtRootVolume{},
EvictionStrategy: EvictionStrategyExternal,
},
expectedJSON: `{"rootVolume":{},"evictionStrategy":"External"}`,
},
{
name: "When EvictionStrategy is LiveMigrate it should be preserved",
current: KubevirtNodePoolPlatform{
RootVolume: &KubevirtRootVolume{},
EvictionStrategy: EvictionStrategyLiveMigrate,
},
expectedJSON: `{"rootVolume":{},"evictionStrategy":"LiveMigrate"}`,
},
{
name: "When EvictionStrategy is None it should be preserved",
current: KubevirtNodePoolPlatform{
RootVolume: &KubevirtRootVolume{},
EvictionStrategy: EvictionStrategyNone,
},
expectedJSON: `{"rootVolume":{},"evictionStrategy":"None"}`,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := json.Marshal(tt.current)
if err != nil {
t.Fatalf("failed to marshal current struct: %v", err)
}
if string(data) != tt.expectedJSON {
t.Errorf("unexpected JSON output: got %s, want %s", string(data), tt.expectedJSON)
}

// Round-trip: unmarshal back into current type
var roundTripped KubevirtNodePoolPlatform
if err := json.Unmarshal(data, &roundTripped); err != nil {
t.Fatalf("failed to round-trip unmarshal: %v", err)
}
if roundTripped.EvictionStrategy != tt.current.EvictionStrategy {
t.Errorf("EvictionStrategy mismatch after round-trip: got %q, want %q", roundTripped.EvictionStrategy, tt.current.EvictionStrategy)
}

// N-1 compatibility: verify current JSON can be parsed by code
// that doesn't know about evictionStrategy (unknown fields ignored)
var rawMap map[string]json.RawMessage
if err := json.Unmarshal(data, &rawMap); err != nil {
t.Fatalf("failed to unmarshal into raw map: %v", err)
}

// N-1 JSON (without evictionStrategy) deserializes into current struct
delete(rawMap, "evictionStrategy")
nMinus1Data, err := json.Marshal(rawMap)
if err != nil {
t.Fatalf("failed to marshal N-1 map: %v", err)
}
var fromNMinus1 KubevirtNodePoolPlatform
if err := json.Unmarshal(nMinus1Data, &fromNMinus1); err != nil {
t.Fatalf("N failed to unmarshal JSON from N-1: %v", err)
}
if fromNMinus1.EvictionStrategy != "" {
t.Errorf("EvictionStrategy should be zero value when deserialized from N-1, got %q", fromNMinus1.EvictionStrategy)
}
})
}
}

func TestKubevirtEvictionStrategyEnumValues(t *testing.T) {
expected := []KubevirtEvictionStrategy{
EvictionStrategyLiveMigrate,
EvictionStrategyLiveMigrateIfPossible,
EvictionStrategyExternal,
EvictionStrategyNone,
}

for _, strategy := range expected {
t.Run("When EvictionStrategy is "+string(strategy)+" it should round-trip through JSON", func(t *testing.T) {
src := KubevirtNodePoolPlatform{
RootVolume: &KubevirtRootVolume{},
EvictionStrategy: strategy,
}
data, err := json.Marshal(src)
if err != nil {
t.Fatalf("failed to marshal: %v", err)
}
var dst KubevirtNodePoolPlatform
if err := json.Unmarshal(data, &dst); err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
if dst.EvictionStrategy != strategy {
t.Errorf("got %q, want %q", dst.EvictionStrategy, strategy)
}
})
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func TestKubevirtNodePoolPlatformDeepCopyPreservesEvictionStrategy(t *testing.T) {
original := &KubevirtNodePoolPlatform{
RootVolume: &KubevirtRootVolume{},
EvictionStrategy: EvictionStrategyExternal,
}
copied := original.DeepCopy()
if !reflect.DeepEqual(original, copied) {
t.Errorf("DeepCopy mismatch: got %+v, want %+v", copied, original)
}
// Mutate copy, verify original unchanged
copied.EvictionStrategy = EvictionStrategyNone
if original.EvictionStrategy != EvictionStrategyExternal {
t.Error("mutating DeepCopy affected original")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1129,6 +1129,28 @@ spec:
- Guaranteed
type: string
type: object
evictionStrategy:
description: |-
evictionStrategy defines the eviction behavior for KubeVirt VMs during
infrastructure node drain. If not set, the cluster-level default from the
KubeVirt configuration applies (typically LiveMigrate on HA clusters when
managed by HCO).

- LiveMigrate: KubeVirt live-migrates the VM to another node (zero guest
disruption). If the VM is not migratable, the node drain stalls.
- LiveMigrateIfPossible: live-migrates if possible, otherwise allows the
VMI pod to be killed (no graceful guest drain).
- External: delegates eviction handling to CAPK, which drains the guest
node before deleting the VMI. Use this for non-migratable VMs (e.g.,
with GPU passthrough or SR-IOV) that need graceful guest node drain.
- None: the VM is killed immediately on eviction with no migration or
graceful guest drain. This is the HCO default on single-node OpenShift.
enum:
- LiveMigrate
- LiveMigrateIfPossible
- External
- None
type: string
hostDevices:
description: |-
hostDevices specifies the host devices (e.g. GPU devices) to be passed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1396,6 +1396,28 @@ spec:
- Guaranteed
type: string
type: object
evictionStrategy:
description: |-
evictionStrategy defines the eviction behavior for KubeVirt VMs during
infrastructure node drain. If not set, the cluster-level default from the
KubeVirt configuration applies (typically LiveMigrate on HA clusters when
managed by HCO).

- LiveMigrate: KubeVirt live-migrates the VM to another node (zero guest
disruption). If the VM is not migratable, the node drain stalls.
- LiveMigrateIfPossible: live-migrates if possible, otherwise allows the
VMI pod to be killed (no graceful guest drain).
- External: delegates eviction handling to CAPK, which drains the guest
node before deleting the VMI. Use this for non-migratable VMs (e.g.,
with GPU passthrough or SR-IOV) that need graceful guest node drain.
- None: the VM is killed immediately on eviction with no migration or
graceful guest drain. This is the HCO default on single-node OpenShift.
enum:
- LiveMigrate
- LiveMigrateIfPossible
- External
- None
type: string
hostDevices:
description: |-
hostDevices specifies the host devices (e.g. GPU devices) to be passed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1162,6 +1162,28 @@ spec:
- Guaranteed
type: string
type: object
evictionStrategy:
description: |-
evictionStrategy defines the eviction behavior for KubeVirt VMs during
infrastructure node drain. If not set, the cluster-level default from the
KubeVirt configuration applies (typically LiveMigrate on HA clusters when
managed by HCO).

- LiveMigrate: KubeVirt live-migrates the VM to another node (zero guest
disruption). If the VM is not migratable, the node drain stalls.
- LiveMigrateIfPossible: live-migrates if possible, otherwise allows the
VMI pod to be killed (no graceful guest drain).
- External: delegates eviction handling to CAPK, which drains the guest
node before deleting the VMI. Use this for non-migratable VMs (e.g.,
with GPU passthrough or SR-IOV) that need graceful guest node drain.
- None: the VM is killed immediately on eviction with no migration or
graceful guest drain. This is the HCO default on single-node OpenShift.
enum:
- LiveMigrate
- LiveMigrateIfPossible
- External
- None
type: string
hostDevices:
description: |-
hostDevices specifies the host devices (e.g. GPU devices) to be passed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1129,6 +1129,28 @@ spec:
- Guaranteed
type: string
type: object
evictionStrategy:
description: |-
evictionStrategy defines the eviction behavior for KubeVirt VMs during
infrastructure node drain. If not set, the cluster-level default from the
KubeVirt configuration applies (typically LiveMigrate on HA clusters when
managed by HCO).

- LiveMigrate: KubeVirt live-migrates the VM to another node (zero guest
disruption). If the VM is not migratable, the node drain stalls.
- LiveMigrateIfPossible: live-migrates if possible, otherwise allows the
VMI pod to be killed (no graceful guest drain).
- External: delegates eviction handling to CAPK, which drains the guest
node before deleting the VMI. Use this for non-migratable VMs (e.g.,
with GPU passthrough or SR-IOV) that need graceful guest node drain.
- None: the VM is killed immediately on eviction with no migration or
graceful guest drain. This is the HCO default on single-node OpenShift.
enum:
- LiveMigrate
- LiveMigrateIfPossible
- External
- None
type: string
hostDevices:
description: |-
hostDevices specifies the host devices (e.g. GPU devices) to be passed
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading