Ctrl K
ComunidadGuías…

api/handler/k8s_attribute_vm_runtime_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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)
}