api/handler/k8s_attribute_vm_runtime_test.go
Rama main · réplica de lectura del repositorio original.
package handler
import (
"context"
"errors"
"testing"
"github.com/golang/mock/gomock"
"github.com/goodrain/rainbond/worker/appm/conversion"
"k8s.io/apimachinery/pkg/api/resource"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
kubevirtv1 "kubevirt.io/api/core/v1"
kubecli "kubevirt.io/client-go/kubecli"
)
func TestApplyVMRuntimeDeviceConfigUpdatesGPUAndUSBDevices(t *testing.T) {
vm := &kubevirtv1.VirtualMachine{
ObjectMeta: metav1.ObjectMeta{
Name: "demo-vm",
Namespace: "demo-ns",
},
Spec: kubevirtv1.VirtualMachineSpec{
Template: &kubevirtv1.VirtualMachineInstanceTemplateSpec{
Spec: kubevirtv1.VirtualMachineInstanceSpec{
Domain: kubevirtv1.DomainSpec{
Devices: kubevirtv1.Devices{},
},
},
},
},
}
updatedVM, changed := applyVMRuntimeDeviceConfig(vm, map[string]string{
"vm_gpu_enabled": "true",
"vm_gpu_resources": "[\"gpu.example.com/A10\"]",
"vm_gpu_count": "2",
"vm_usb_enabled": "true",
"vm_usb_resources": "[\"kubevirt.io/usb-a\"]",
})
if !changed {
t.Fatal("expected runtime device config to change VM devices")
}
if len(updatedVM.Spec.Template.Spec.Domain.Devices.GPUs) != 2 {
t.Fatalf("expected 2 gpu devices, got %#v", updatedVM.Spec.Template.Spec.Domain.Devices.GPUs)
}
if updatedVM.Spec.Template.Spec.Domain.Devices.GPUs[0].DeviceName != "gpu.example.com/A10" {
t.Fatalf("unexpected gpu config: %#v", updatedVM.Spec.Template.Spec.Domain.Devices.GPUs)
}
if len(updatedVM.Spec.Template.Spec.Domain.Devices.HostDevices) != 1 {
t.Fatalf("expected 1 usb host device, got %#v", updatedVM.Spec.Template.Spec.Domain.Devices.HostDevices)
}
if updatedVM.Spec.Template.Spec.Domain.Devices.HostDevices[0].DeviceName != "kubevirt.io/usb-a" {
t.Fatalf("unexpected host device config: %#v", updatedVM.Spec.Template.Spec.Domain.Devices.HostDevices)
}
}
func TestApplyVMRuntimeDeviceConfigClearsAcceleratorsWhenDisabled(t *testing.T) {
vm := &kubevirtv1.VirtualMachine{
ObjectMeta: metav1.ObjectMeta{
Name: "demo-vm",
Namespace: "demo-ns",
},
Spec: kubevirtv1.VirtualMachineSpec{
Template: &kubevirtv1.VirtualMachineInstanceTemplateSpec{
Spec: kubevirtv1.VirtualMachineInstanceSpec{
Domain: kubevirtv1.DomainSpec{
Devices: kubevirtv1.Devices{
GPUs: []kubevirtv1.GPU{
{Name: "gpu-0", DeviceName: "gpu.example.com/A10"},
},
HostDevices: []kubevirtv1.HostDevice{
{Name: "usb-0", DeviceName: "kubevirt.io/usb-a"},
},
},
},
},
},
},
}
updatedVM, changed := applyVMRuntimeDeviceConfig(vm, map[string]string{})
if !changed {
t.Fatal("expected runtime device config to clear VM devices")
}
if len(updatedVM.Spec.Template.Spec.Domain.Devices.GPUs) != 0 {
t.Fatalf("expected gpus to be cleared, got %#v", updatedVM.Spec.Template.Spec.Domain.Devices.GPUs)
}
if len(updatedVM.Spec.Template.Spec.Domain.Devices.HostDevices) != 0 {
t.Fatalf("expected usb host devices to be cleared, got %#v", updatedVM.Spec.Template.Spec.Domain.Devices.HostDevices)
}
}
func TestSyncVMRuntimeDeviceConfigUpdatesVirtualMachineWhenDevicesChange(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockClient := kubecli.NewMockKubevirtClient(ctrl)
mockVMInterface := kubecli.NewMockVirtualMachineInterface(ctrl)
vm := &kubevirtv1.VirtualMachine{
ObjectMeta: metav1.ObjectMeta{
Name: "demo-vm",
Namespace: "demo-ns",
},
Spec: kubevirtv1.VirtualMachineSpec{
Template: &kubevirtv1.VirtualMachineInstanceTemplateSpec{
Spec: kubevirtv1.VirtualMachineInstanceSpec{
Domain: kubevirtv1.DomainSpec{
Devices: kubevirtv1.Devices{},
},
},
},
},
}
expectedGPUs, expectedHostDevices := conversion.BuildVMAccelerationDevices(map[string]string{
"vm_gpu_enabled": "true",
"vm_gpu_resources": "[\"gpu.example.com/A10\"]",
"vm_usb_enabled": "true",
"vm_usb_resources": "[\"kubevirt.io/usb-a\"]",
})
mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMInterface)
mockVMInterface.EXPECT().Update(gomock.Any(), gomock.AssignableToTypeOf(&kubevirtv1.VirtualMachine{}), gomock.Any()).DoAndReturn(
func(_ context.Context, updated *kubevirtv1.VirtualMachine, _ metav1.UpdateOptions) (*kubevirtv1.VirtualMachine, error) {
if len(updated.Spec.Template.Spec.Domain.Devices.GPUs) != len(expectedGPUs) {
t.Fatalf("expected updated vm to carry gpu config, got %#v", updated.Spec.Template.Spec.Domain.Devices.GPUs)
}
if len(updated.Spec.Template.Spec.Domain.Devices.HostDevices) != len(expectedHostDevices) {
t.Fatalf("expected updated vm to carry usb config, got %#v", updated.Spec.Template.Spec.Domain.Devices.HostDevices)
}
return updated, nil
},
)
action := &ServiceAction{kubevirtClient: mockClient}
if err := action.syncVMRuntimeDeviceConfig(vm, map[string]string{
"vm_gpu_enabled": "true",
"vm_gpu_resources": "[\"gpu.example.com/A10\"]",
"vm_usb_enabled": "true",
"vm_usb_resources": "[\"kubevirt.io/usb-a\"]",
}); err != nil {
t.Fatalf("expected sync to succeed, got %v", err)
}
}
func TestSyncVMRuntimeDeviceConfigSkipsUpdateWhenDevicesAlreadyMatch(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockClient := kubecli.NewMockKubevirtClient(ctrl)
gpus, hostDevices := conversion.BuildVMAccelerationDevices(map[string]string{
"vm_gpu_enabled": "true",
"vm_gpu_resources": "[\"gpu.example.com/A10\"]",
"vm_usb_enabled": "true",
"vm_usb_resources": "[\"kubevirt.io/usb-a\"]",
})
vm := &kubevirtv1.VirtualMachine{
ObjectMeta: metav1.ObjectMeta{
Name: "demo-vm",
Namespace: "demo-ns",
},
Spec: kubevirtv1.VirtualMachineSpec{
Template: &kubevirtv1.VirtualMachineInstanceTemplateSpec{
Spec: kubevirtv1.VirtualMachineInstanceSpec{
Domain: kubevirtv1.DomainSpec{
Devices: kubevirtv1.Devices{
GPUs: gpus,
HostDevices: hostDevices,
},
},
},
},
},
}
action := &ServiceAction{kubevirtClient: mockClient}
if err := action.syncVMRuntimeDeviceConfig(vm, map[string]string{
"vm_gpu_enabled": "true",
"vm_gpu_resources": "[\"gpu.example.com/A10\"]",
"vm_usb_enabled": "true",
"vm_usb_resources": "[\"kubevirt.io/usb-a\"]",
}); err != nil {
t.Fatalf("expected sync to succeed without update, got %v", err)
}
}
func TestApplyVirtualMachineSpecPreservesRunStrategyWhenSyncingSpec(t *testing.T) {
existing := &kubevirtv1.VirtualMachine{
ObjectMeta: metav1.ObjectMeta{
Name: "demo-vm",
Namespace: "demo-ns",
},
Spec: kubevirtv1.VirtualMachineSpec{
RunStrategy: pointerToRunStrategy(kubevirtv1.RunStrategyHalted),
Template: &kubevirtv1.VirtualMachineInstanceTemplateSpec{
Spec: kubevirtv1.VirtualMachineInstanceSpec{
Domain: kubevirtv1.DomainSpec{
Memory: &kubevirtv1.Memory{},
},
},
},
},
}
targetMemory := resourceMustParse(t, "4Gi")
desired := &kubevirtv1.VirtualMachine{
ObjectMeta: metav1.ObjectMeta{
Name: "demo-vm",
Namespace: "demo-ns",
},
Spec: kubevirtv1.VirtualMachineSpec{
RunStrategy: pointerToRunStrategy(kubevirtv1.RunStrategyAlways),
Template: &kubevirtv1.VirtualMachineInstanceTemplateSpec{
Spec: kubevirtv1.VirtualMachineInstanceSpec{
Domain: kubevirtv1.DomainSpec{
Memory: &kubevirtv1.Memory{
Guest: &targetMemory,
},
},
},
},
},
}
updated, changed := applyVirtualMachineSpec(existing, desired)
if !changed {
t.Fatal("expected virtual machine spec diff to be detected")
}
if updated.Spec.RunStrategy == nil || *updated.Spec.RunStrategy != kubevirtv1.RunStrategyHalted {
t.Fatalf("expected run strategy to be preserved, got %#v", updated.Spec.RunStrategy)
}
if updated.Spec.Template == nil || updated.Spec.Template.Spec.Domain.Memory == nil || updated.Spec.Template.Spec.Domain.Memory.Guest == nil {
t.Fatalf("expected target memory to be synced, got %#v", updated.Spec.Template)
}
if updated.Spec.Template.Spec.Domain.Memory.Guest.Cmp(targetMemory) != 0 {
t.Fatalf("expected target memory to be synced, got %#v", updated.Spec.Template.Spec.Domain.Memory.Guest)
}
}
func TestApplyVirtualMachineSpecPreservesLegacyRunningFlagWhenSyncingSpec(t *testing.T) {
existing := &kubevirtv1.VirtualMachine{
ObjectMeta: metav1.ObjectMeta{
Name: "demo-vm",
Namespace: "demo-ns",
},
Spec: kubevirtv1.VirtualMachineSpec{
Running: pointerToBool(false),
Template: &kubevirtv1.VirtualMachineInstanceTemplateSpec{
Spec: kubevirtv1.VirtualMachineInstanceSpec{
Domain: kubevirtv1.DomainSpec{
Memory: &kubevirtv1.Memory{},
},
},
},
},
}
targetMemory := resourceMustParse(t, "2Gi")
desired := &kubevirtv1.VirtualMachine{
ObjectMeta: metav1.ObjectMeta{
Name: "demo-vm",
Namespace: "demo-ns",
},
Spec: kubevirtv1.VirtualMachineSpec{
RunStrategy: pointerToRunStrategy(kubevirtv1.RunStrategyAlways),
Template: &kubevirtv1.VirtualMachineInstanceTemplateSpec{
Spec: kubevirtv1.VirtualMachineInstanceSpec{
Domain: kubevirtv1.DomainSpec{
Memory: &kubevirtv1.Memory{
Guest: &targetMemory,
},
},
},
},
},
}
updated, changed := applyVirtualMachineSpec(existing, desired)
if !changed {
t.Fatal("expected virtual machine spec diff to be detected")
}
if updated.Spec.Running == nil || *updated.Spec.Running {
t.Fatalf("expected legacy running flag to stay false, got %#v", updated.Spec.Running)
}
if updated.Spec.RunStrategy != nil {
t.Fatalf("expected legacy running flag sync to avoid forcing runStrategy, got %#v", updated.Spec.RunStrategy)
}
if updated.Spec.Template == nil || updated.Spec.Template.Spec.Domain.Memory == nil || updated.Spec.Template.Spec.Domain.Memory.Guest == nil {
t.Fatalf("expected target memory to be synced, got %#v", updated.Spec.Template)
}
if updated.Spec.Template.Spec.Domain.Memory.Guest.Cmp(targetMemory) != 0 {
t.Fatalf("expected target memory to be synced, got %#v", updated.Spec.Template.Spec.Domain.Memory.Guest)
}
}
func TestSyncVirtualMachineSpecUpdatesWhenSpecChanges(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockClient := kubecli.NewMockKubevirtClient(ctrl)
mockVMInterface := kubecli.NewMockVirtualMachineInterface(ctrl)
existing := &kubevirtv1.VirtualMachine{
ObjectMeta: metav1.ObjectMeta{
Name: "demo-vm",
Namespace: "demo-ns",
},
Spec: kubevirtv1.VirtualMachineSpec{
RunStrategy: pointerToRunStrategy(kubevirtv1.RunStrategyHalted),
Template: &kubevirtv1.VirtualMachineInstanceTemplateSpec{
Spec: kubevirtv1.VirtualMachineInstanceSpec{
Domain: kubevirtv1.DomainSpec{
Memory: &kubevirtv1.Memory{},
},
},
},
},
}
targetMemory := resourceMustParse(t, "4Gi")
desired := &kubevirtv1.VirtualMachine{
ObjectMeta: metav1.ObjectMeta{
Name: "demo-vm",
Namespace: "demo-ns",
},
Spec: kubevirtv1.VirtualMachineSpec{
RunStrategy: pointerToRunStrategy(kubevirtv1.RunStrategyAlways),
Template: &kubevirtv1.VirtualMachineInstanceTemplateSpec{
Spec: kubevirtv1.VirtualMachineInstanceSpec{
Domain: kubevirtv1.DomainSpec{
Memory: &kubevirtv1.Memory{
Guest: &targetMemory,
},
},
},
},
},
}
mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMInterface).Times(2)
mockVMInterface.EXPECT().Get(gomock.Any(), "demo-vm", gomock.Any()).DoAndReturn(
func(_ context.Context, _ string, _ metav1.GetOptions) (*kubevirtv1.VirtualMachine, error) {
return existing.DeepCopy(), nil
},
)
mockVMInterface.EXPECT().Update(gomock.Any(), gomock.AssignableToTypeOf(&kubevirtv1.VirtualMachine{}), gomock.Any()).DoAndReturn(
func(_ context.Context, updated *kubevirtv1.VirtualMachine, _ metav1.UpdateOptions) (*kubevirtv1.VirtualMachine, error) {
if updated.Spec.RunStrategy == nil || *updated.Spec.RunStrategy != kubevirtv1.RunStrategyHalted {
t.Fatalf("expected updated vm run strategy to stay halted, got %#v", updated.Spec.RunStrategy)
}
if updated.Spec.Template == nil || updated.Spec.Template.Spec.Domain.Memory == nil || updated.Spec.Template.Spec.Domain.Memory.Guest == nil {
t.Fatalf("expected updated vm to carry memory config, got %#v", updated.Spec.Template)
}
if updated.Spec.Template.Spec.Domain.Memory.Guest.Cmp(targetMemory) != 0 {
t.Fatalf("expected updated vm to carry memory config, got %#v", updated.Spec.Template.Spec.Domain.Memory.Guest)
}
return updated, nil
},
)
action := &ServiceAction{kubevirtClient: mockClient}
if err := action.syncVirtualMachineSpec(existing, desired); err != nil {
t.Fatalf("expected virtual machine spec sync to succeed, got %v", err)
}
}
// capability_id: rainbond.vm-runtime-spec-sync-conflict-retry
func TestSyncVirtualMachineSpecRetriesOnConflict(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockClient := kubecli.NewMockKubevirtClient(ctrl)
mockVMInterface := kubecli.NewMockVirtualMachineInterface(ctrl)
existing := &kubevirtv1.VirtualMachine{
ObjectMeta: metav1.ObjectMeta{
Name: "demo-vm",
Namespace: "demo-ns",
},
Spec: kubevirtv1.VirtualMachineSpec{
RunStrategy: pointerToRunStrategy(kubevirtv1.RunStrategyHalted),
Template: &kubevirtv1.VirtualMachineInstanceTemplateSpec{
Spec: kubevirtv1.VirtualMachineInstanceSpec{
Domain: kubevirtv1.DomainSpec{
Memory: &kubevirtv1.Memory{},
},
},
},
},
}
desiredMemory := resourceMustParse(t, "4Gi")
desired := &kubevirtv1.VirtualMachine{
ObjectMeta: metav1.ObjectMeta{
Name: "demo-vm",
Namespace: "demo-ns",
},
Spec: kubevirtv1.VirtualMachineSpec{
RunStrategy: pointerToRunStrategy(kubevirtv1.RunStrategyAlways),
Template: &kubevirtv1.VirtualMachineInstanceTemplateSpec{
Spec: kubevirtv1.VirtualMachineInstanceSpec{
Domain: kubevirtv1.DomainSpec{
Memory: &kubevirtv1.Memory{
Guest: &desiredMemory,
},
},
},
},
},
}
mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMInterface).Times(4)
mockVMInterface.EXPECT().Get(gomock.Any(), "demo-vm", gomock.Any()).Times(2).DoAndReturn(
func(_ context.Context, _ string, _ metav1.GetOptions) (*kubevirtv1.VirtualMachine, error) {
return existing.DeepCopy(), nil
},
)
attempts := 0
mockVMInterface.EXPECT().Update(gomock.Any(), gomock.AssignableToTypeOf(&kubevirtv1.VirtualMachine{}), gomock.Any()).Times(2).DoAndReturn(
func(_ context.Context, updated *kubevirtv1.VirtualMachine, _ metav1.UpdateOptions) (*kubevirtv1.VirtualMachine, error) {
attempts++
if updated.Spec.Template == nil || updated.Spec.Template.Spec.Domain.Memory == nil || updated.Spec.Template.Spec.Domain.Memory.Guest == nil {
t.Fatalf("expected updated vm to carry memory config, got %#v", updated.Spec.Template)
}
if updated.Spec.Template.Spec.Domain.Memory.Guest.Cmp(desiredMemory) != 0 {
t.Fatalf("expected updated vm to carry target memory, got %#v", updated.Spec.Template.Spec.Domain.Memory.Guest)
}
if attempts == 1 {
return nil, k8serrors.NewConflict(
schema.GroupResource{Group: "kubevirt.io", Resource: "virtualmachines"},
"demo-vm",
errors.New("the object has been modified"),
)
}
return updated, nil
},
)
action := &ServiceAction{kubevirtClient: mockClient}
if err := action.syncVirtualMachineSpec(existing, desired); err != nil {
t.Fatalf("expected conflict retry to succeed, got %v", err)
}
if attempts != 2 {
t.Fatalf("expected vm spec update to retry once after conflict, got %d attempts", attempts)
}
}
func TestIsVMRuntimeSpecAttributeOnlyIncludesSupportedVMFields(t *testing.T) {
for _, name := range []string{
"vm_os_name",
} {
if !isVMRuntimeSpecAttribute(name) {
t.Fatalf("expected %s to trigger vm spec sync", name)
}
}
for _, name := range []string{
"vm_network_mode",
"vm_network_name",
"vm_fixed_ip",
"vm_gateway",
"vm_dns_servers",
} {
if isVMRuntimeSpecAttribute(name) {
t.Fatalf("did not expect removed vm network field %s to trigger vm spec sync", name)
}
}
}
// capability_id: rainbond.vm-runtime.disk-layout-attr-triggers-spec-sync
func TestIsVMRuntimeSpecAttributeIncludesDiskLayout(t *testing.T) {
if !isVMRuntimeSpecAttribute("vm_disk_layout") {
t.Fatal("expected vm_disk_layout to trigger vm spec sync")
}
}
// capability_id: rainbond.vm-probe-attribute-syncs-spec
func TestIsVMRuntimeSpecAttributeIncludesProbeAttributes(t *testing.T) {
for _, name := range []string{
"livenessProbe",
"readinessProbe",
} {
if !isVMRuntimeSpecAttribute(name) {
t.Fatalf("expected %s to trigger vm spec sync", name)
}
}
}
func pointerToRunStrategy(strategy kubevirtv1.VirtualMachineRunStrategy) *kubevirtv1.VirtualMachineRunStrategy {
return &strategy
}
func pointerToBool(value bool) *bool {
return &value
}
func resourceMustParse(t *testing.T, value string) resource.Quantity {
t.Helper()
return resource.MustParse(value)
}