package handler import ( "context" "errors" "strings" "testing" "github.com/golang/mock/gomock" "github.com/goodrain/rainbond/db" dbmodel "github.com/goodrain/rainbond/db/model" workermodel "github.com/goodrain/rainbond/worker/discover/model" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" kubefake "k8s.io/client-go/kubernetes/fake" v1 "kubevirt.io/api/core/v1" kubecli "kubevirt.io/client-go/kubecli" ) func TestServiceVerticalVMLiveUpdatePatchesRunningVM(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() serviceDao := &resourceSyncTenantServiceDao{ service: &dbmodel.TenantServices{ ServiceID: "service-vm", ServiceAlias: "service-vm", ExtendMethod: "vm", ContainerCPU: 6000, ContainerMemory: 12288, ContainerGPU: 0, }, } eventDao := &resourceSyncEventDao{} db.SetTestManager(resourceSyncTestManager{ serviceDao: serviceDao, eventDao: eventDao, }) defer db.SetTestManager(nil) mockClient := kubecli.NewMockKubevirtClient(ctrl) mockVMList := kubecli.NewMockVirtualMachineInterface(ctrl) mockVMPatch := kubecli.NewMockVirtualMachineInterface(ctrl) mockVMIList := kubecli.NewMockVirtualMachineInstanceInterface(ctrl) mockKV := kubecli.NewMockKubeVirtInterface(ctrl) mockClient.EXPECT().VirtualMachine("").Return(mockVMList) mockVMList.EXPECT().List(gomock.Any(), metav1.ListOptions{LabelSelector: "service_id=service-vm"}).Return(&v1.VirtualMachineList{ Items: []v1.VirtualMachine{ { ObjectMeta: metav1.ObjectMeta{Name: "demo-vm", Namespace: "demo-ns"}, Spec: v1.VirtualMachineSpec{ Template: &v1.VirtualMachineInstanceTemplateSpec{ Spec: v1.VirtualMachineInstanceSpec{ Domain: v1.DomainSpec{ CPU: &v1.CPU{ Sockets: 6, Cores: 1, Threads: 1, MaxSockets: 8, }, Memory: &v1.Memory{ Guest: quantityPtr("12Gi"), MaxGuest: quantityPtr("16Gi"), }, }, }, }, }, Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning}, }, }, }, nil) mockClient.EXPECT().VirtualMachineInstance("").Return(mockVMIList) mockVMIList.EXPECT().List(gomock.Any(), metav1.ListOptions{LabelSelector: "service_id=service-vm"}).Return(&v1.VirtualMachineInstanceList{ Items: []v1.VirtualMachineInstance{ { ObjectMeta: metav1.ObjectMeta{Name: "demo-vm", Namespace: "demo-ns"}, Status: v1.VirtualMachineInstanceStatus{ Phase: v1.Running, Conditions: []v1.VirtualMachineInstanceCondition{ {Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"}, }, }, }, }, }, nil) mockClient.EXPECT().KubeVirt("").Return(mockKV) mockKV.EXPECT().List(gomock.Any(), gomock.Any()).Return(&v1.KubeVirtList{ Items: []v1.KubeVirt{ { Spec: v1.KubeVirtSpec{ WorkloadUpdateStrategy: v1.KubeVirtWorkloadUpdateStrategy{ WorkloadUpdateMethods: []v1.WorkloadUpdateMethod{v1.WorkloadUpdateMethodLiveMigrate}, }, Configuration: v1.KubeVirtConfiguration{ VMRolloutStrategy: func() *v1.VMRolloutStrategy { strategy := v1.VMRolloutStrategyLiveUpdate return &strategy }(), }, }, }, }, }, nil) mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMPatch) mockVMPatch.EXPECT().Patch(gomock.Any(), "demo-vm", types.JSONPatchType, gomock.Any(), gomock.Any()).DoAndReturn( func(_ context.Context, _ string, _ types.PatchType, data []byte, _ metav1.PatchOptions, _ ...string) (*v1.VirtualMachine, error) { payload := string(data) if !strings.Contains(payload, "/spec/template/spec/domain/cpu/sockets") { t.Fatalf("expected cpu sockets patch, got %s", payload) } return &v1.VirtualMachine{}, nil }, ) action := &ServiceAction{ MQClient: &noopMQClient{}, kubevirtClient: mockClient, } newCPU := 7000 memory := 12288 err := action.ServiceVertical(context.Background(), &workermodel.VerticalScalingTaskBody{ ServiceID: "service-vm", ContainerCPU: &newCPU, ContainerMemory: &memory, }) if err != nil { t.Fatalf("expected no error, got %v", err) } if serviceDao.updatedService == nil || serviceDao.updatedService.ContainerCPU != 7000 { t.Fatalf("expected updated cpu persisted, got %#v", serviceDao.updatedService) } } func TestServiceVerticalVMStoppedFallsBackToSpecSync(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() serviceDao := &resourceSyncTenantServiceDao{ service: &dbmodel.TenantServices{ ServiceID: "service-vm", ServiceAlias: "service-vm", ExtendMethod: "vm", ContainerCPU: 4000, ContainerMemory: 8192, ContainerGPU: 0, }, } db.SetTestManager(resourceSyncTestManager{ serviceDao: serviceDao, eventDao: &resourceSyncEventDao{}, }) defer db.SetTestManager(nil) mockClient := kubecli.NewMockKubevirtClient(ctrl) mockVMList := kubecli.NewMockVirtualMachineInterface(ctrl) mockClient.EXPECT().VirtualMachine("").Return(mockVMList) mockVMList.EXPECT().List(gomock.Any(), metav1.ListOptions{LabelSelector: "service_id=service-vm"}).Return(&v1.VirtualMachineList{ Items: []v1.VirtualMachine{ { ObjectMeta: metav1.ObjectMeta{Name: "demo-vm", Namespace: "demo-ns"}, Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusStopped}, }, }, }, nil) syncedServiceID := "" action := &ServiceAction{ MQClient: &noopMQClient{}, kubevirtClient: mockClient, syncVirtualMachineSpecHook: func(serviceID string) error { syncedServiceID = serviceID return nil }, } newCPU := 5000 newMemory := 10240 err := action.ServiceVertical(context.Background(), &workermodel.VerticalScalingTaskBody{ ServiceID: "service-vm", ContainerCPU: &newCPU, ContainerMemory: &newMemory, }) if err != nil { t.Fatalf("expected no error, got %v", err) } if syncedServiceID != "service-vm" { t.Fatalf("expected vm spec sync for stopped VM, got %q", syncedServiceID) } } // capability_id: rainbond.vm-live-update.unsupported-auto-restart func TestServiceVerticalVMNonMigratableFallsBackToSpecSyncAndRestart(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() serviceDao := &resourceSyncTenantServiceDao{ service: &dbmodel.TenantServices{ ServiceID: "service-vm", ServiceAlias: "service-vm", ExtendMethod: "vm", ContainerCPU: 4000, ContainerMemory: 8192, ContainerGPU: 0, }, } eventDao := &resourceSyncEventDao{} db.SetTestManager(resourceSyncTestManager{ serviceDao: serviceDao, eventDao: eventDao, }) defer db.SetTestManager(nil) mockClient := kubecli.NewMockKubevirtClient(ctrl) mockVMInterface := kubecli.NewMockVirtualMachineInterface(ctrl) syncedServiceID := "" action := &ServiceAction{ MQClient: &noopMQClient{}, kubevirtClient: mockClient, syncVirtualMachineSpecHook: func(serviceID string) error { syncedServiceID = serviceID return nil }, } action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) { return &v1.VirtualMachine{ ObjectMeta: metav1.ObjectMeta{Name: "demo-vm", Namespace: "demo-ns"}, Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning}, }, nil } action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) { return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{ Phase: v1.Running, Conditions: []v1.VirtualMachineInstanceCondition{ { Type: v1.VirtualMachineInstanceIsMigratable, Status: "False", Message: "PVC manual82 is not shared", }, }, }}, nil } mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMInterface) mockVMInterface.EXPECT().Restart(gomock.Any(), "demo-vm", gomock.Any()).Return(nil) newMemory := 10240 err := action.ServiceVertical(context.Background(), &workermodel.VerticalScalingTaskBody{ ServiceID: "service-vm", ContainerMemory: &newMemory, }) if err != nil { t.Fatalf("expected non-migratable running VM to restart instead of failing live update, got %v", err) } if syncedServiceID != "service-vm" { t.Fatalf("expected vm spec sync before restart, got %q", syncedServiceID) } if serviceDao.updatedService == nil || serviceDao.updatedService.ContainerMemory != 10240 { t.Fatalf("expected updated memory to persist, got %#v", serviceDao.updatedService) } if len(eventDao.statuses) == 0 || eventDao.statuses[len(eventDao.statuses)-1] != dbmodel.EventStatusSuccess { t.Fatalf("expected success event status, got %#v", eventDao.statuses) } } func TestServiceVerticalVMFixedPodIPFallsBackToSpecSyncAndRestart(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() serviceDao := &resourceSyncTenantServiceDao{ service: &dbmodel.TenantServices{ ServiceID: "service-vm", ServiceAlias: "service-vm", ExtendMethod: "vm", ContainerCPU: 4000, ContainerMemory: 8192, ContainerGPU: 0, }, } eventDao := &resourceSyncEventDao{} db.SetTestManager(resourceSyncTestManager{ serviceDao: serviceDao, eventDao: eventDao, }) defer db.SetTestManager(nil) mockClient := kubecli.NewMockKubevirtClient(ctrl) mockVMInterface := kubecli.NewMockVirtualMachineInterface(ctrl) syncedServiceID := "" action := &ServiceAction{ MQClient: &noopMQClient{}, kubevirtClient: mockClient, syncVirtualMachineSpecHook: func(serviceID string) error { syncedServiceID = serviceID return nil }, } action.loadVMRuntimeSpecExtensionSetHook = func(componentID string) (map[string]string, error) { if componentID != "service-vm" { t.Fatalf("unexpected component id %q", componentID) } return map[string]string{ "vm_fixed_ip_enabled": "true", "vm_fixed_ip": "10.42.247.130", }, nil } action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) { return &v1.VirtualMachine{ ObjectMeta: metav1.ObjectMeta{Name: "demo-vm", Namespace: "demo-ns"}, Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning}, }, nil } mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMInterface) mockVMInterface.EXPECT().Restart(gomock.Any(), "demo-vm", gomock.Any()).Return(nil) newMemory := 10240 err := action.ServiceVertical(context.Background(), &workermodel.VerticalScalingTaskBody{ ServiceID: "service-vm", ContainerMemory: &newMemory, }) if err != nil { t.Fatalf("expected fixed pod ip VM to restart instead of live update, got %v", err) } if syncedServiceID != "service-vm" { t.Fatalf("expected vm spec sync before restart, got %q", syncedServiceID) } if serviceDao.updatedService == nil || serviceDao.updatedService.ContainerMemory != 10240 { t.Fatalf("expected updated memory to persist, got %#v", serviceDao.updatedService) } if len(eventDao.statuses) == 0 || eventDao.statuses[len(eventDao.statuses)-1] != dbmodel.EventStatusSuccess { t.Fatalf("expected success event status, got %#v", eventDao.statuses) } } // capability_id: rainbond.vm-live-update.unsupported-auto-restart func TestServiceVerticalVMPatchMigrationErrorFallsBackToSpecSyncAndRestart(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() serviceDao := &resourceSyncTenantServiceDao{ service: &dbmodel.TenantServices{ ServiceID: "service-vm", ServiceAlias: "service-vm", ExtendMethod: "vm", ContainerCPU: 4000, ContainerMemory: 8192, ContainerGPU: 0, }, } eventDao := &resourceSyncEventDao{} db.SetTestManager(resourceSyncTestManager{ serviceDao: serviceDao, eventDao: eventDao, }) defer db.SetTestManager(nil) mockClient := kubecli.NewMockKubevirtClient(ctrl) mockVMInterface := kubecli.NewMockVirtualMachineInterface(ctrl) syncedServiceID := "" action := &ServiceAction{ MQClient: &noopMQClient{}, kubevirtClient: mockClient, syncVirtualMachineSpecHook: func(serviceID string) error { syncedServiceID = serviceID return nil }, } action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true } action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) { return &v1.VirtualMachine{ ObjectMeta: metav1.ObjectMeta{Name: "demo-vm", Namespace: "demo-ns"}, Spec: v1.VirtualMachineSpec{ Template: &v1.VirtualMachineInstanceTemplateSpec{ Spec: v1.VirtualMachineInstanceSpec{ Domain: v1.DomainSpec{ Memory: &v1.Memory{ Guest: quantityPtr("8Gi"), MaxGuest: quantityPtr("16Gi"), }, }, }, }, }, Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning}, }, nil } action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) { return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{ Phase: v1.Running, Conditions: []v1.VirtualMachineInstanceCondition{ {Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"}, }, }}, nil } mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMInterface).Times(2) mockVMInterface.EXPECT().Patch(gomock.Any(), "demo-vm", types.JSONPatchType, gomock.Any(), gomock.Any()). Return(nil, errors.New("cannot migrate VMI: PVC manual82 is not shared, live migration requires that all PVCs must be shared (using ReadWriteMany access mode)")) mockVMInterface.EXPECT().Restart(gomock.Any(), "demo-vm", gomock.Any()).Return(nil) newMemory := 10240 err := action.ServiceVertical(context.Background(), &workermodel.VerticalScalingTaskBody{ ServiceID: "service-vm", ContainerMemory: &newMemory, }) if err != nil { t.Fatalf("expected live migration pvc error to restart instead of failing vertical update, got %v", err) } if syncedServiceID != "service-vm" { t.Fatalf("expected vm spec sync before restart, got %q", syncedServiceID) } if serviceDao.updatedService == nil || serviceDao.updatedService.ContainerMemory != 10240 { t.Fatalf("expected updated memory to persist, got %#v", serviceDao.updatedService) } if len(eventDao.statuses) == 0 || eventDao.statuses[len(eventDao.statuses)-1] != dbmodel.EventStatusSuccess { t.Fatalf("expected success event status, got %#v", eventDao.statuses) } } // capability_id: rainbond.vm-live-update.cpu-memory-combined-rejected func TestServiceVerticalVMLiveUpdateRejectsCombinedCPUAndMemoryChange(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() serviceDao := &resourceSyncTenantServiceDao{ service: &dbmodel.TenantServices{ ServiceID: "service-vm", ServiceAlias: "service-vm", ExtendMethod: "vm", ContainerCPU: 8000, ContainerMemory: 16384, ContainerGPU: 0, }, } eventDao := &resourceSyncEventDao{} db.SetTestManager(resourceSyncTestManager{ serviceDao: serviceDao, eventDao: eventDao, }) defer db.SetTestManager(nil) syncedServiceID := "" action := &ServiceAction{ MQClient: &noopMQClient{}, kubevirtClient: kubecli.NewMockKubevirtClient(ctrl), syncVirtualMachineSpecHook: func(serviceID string) error { syncedServiceID = serviceID return nil }, } action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true } action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) { return &v1.VirtualMachine{ ObjectMeta: metav1.ObjectMeta{Name: "demo-vm", Namespace: "demo-ns"}, Spec: v1.VirtualMachineSpec{ Template: &v1.VirtualMachineInstanceTemplateSpec{ Spec: v1.VirtualMachineInstanceSpec{ Domain: v1.DomainSpec{ CPU: &v1.CPU{ Sockets: 8, Cores: 1, Threads: 1, MaxSockets: 16, }, Memory: &v1.Memory{ Guest: quantityPtr("16Gi"), MaxGuest: quantityPtr("64Gi"), }, }, }, }, }, Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning}, }, nil } action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) { return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{ Phase: v1.Running, Conditions: []v1.VirtualMachineInstanceCondition{ {Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"}, }, }}, nil } newCPU := 10000 newMemory := 24576 err := action.ServiceVertical(context.Background(), &workermodel.VerticalScalingTaskBody{ ServiceID: "service-vm", ContainerCPU: &newCPU, ContainerMemory: &newMemory, }) if err == nil { t.Fatal("expected combined cpu and memory live update to be rejected") } if err.Error() != "运行中虚拟机 CPU 和内存热更新请分两次操作,不支持一次同时修改。" { t.Fatalf("expected combined resource rejection message, got %q", err.Error()) } if syncedServiceID != "service-vm" { t.Fatalf("expected rollback vm spec sync, got %q", syncedServiceID) } if serviceDao.service == nil || serviceDao.service.ContainerCPU != 8000 || serviceDao.service.ContainerMemory != 16384 { t.Fatalf("expected resources rollback to original values, got %#v", serviceDao.service) } if len(eventDao.statuses) == 0 || eventDao.statuses[len(eventDao.statuses)-1] != dbmodel.EventStatusFailure { t.Fatalf("expected failure event status, got %#v", eventDao.statuses) } } // capability_id: rainbond.vm-live-update.running-cpu-shrink-rejected func TestServiceVerticalVMLiveUpdateRejectsRunningVMCPUShrink(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() serviceDao := &resourceSyncTenantServiceDao{ service: &dbmodel.TenantServices{ ServiceID: "service-vm", ServiceAlias: "service-vm", ExtendMethod: "vm", ContainerCPU: 6000, ContainerMemory: 12288, ContainerGPU: 0, }, } eventDao := &resourceSyncEventDao{} db.SetTestManager(resourceSyncTestManager{ serviceDao: serviceDao, eventDao: eventDao, }) defer db.SetTestManager(nil) syncedServiceID := "" action := &ServiceAction{ MQClient: &noopMQClient{}, kubevirtClient: kubecli.NewMockKubevirtClient(ctrl), syncVirtualMachineSpecHook: func(serviceID string) error { syncedServiceID = serviceID return nil }, } action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true } action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) { return &v1.VirtualMachine{ Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning}, }, nil } action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) { return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{ Phase: v1.Running, Conditions: []v1.VirtualMachineInstanceCondition{ {Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"}, }, }}, nil } newCPU := 4000 err := action.ServiceVertical(context.Background(), &workermodel.VerticalScalingTaskBody{ ServiceID: "service-vm", ContainerCPU: &newCPU, }) if err == nil { t.Fatal("expected running vm cpu shrink to be rejected") } if err.Error() != "虚拟机 CPU 热更新仅支持扩容,不支持缩容,请停机后再修改规格。" { t.Fatalf("expected localized rejection message, got %q", err.Error()) } if syncedServiceID != "service-vm" { t.Fatalf("expected rollback vm spec sync, got %q", syncedServiceID) } if serviceDao.service == nil || serviceDao.service.ContainerCPU != 6000 { t.Fatalf("expected cpu rollback to original value, got %#v", serviceDao.service) } if len(eventDao.statuses) == 0 || eventDao.statuses[len(eventDao.statuses)-1] != dbmodel.EventStatusFailure { t.Fatalf("expected failure event status, got %#v", eventDao.statuses) } } func TestServiceVerticalWindowsVMLiveUpdateFallsBackToSpecSyncAndRestart(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() serviceDao := &resourceSyncTenantServiceDao{ service: &dbmodel.TenantServices{ ServiceID: "service-vm", ServiceAlias: "service-vm", ExtendMethod: "vm", ContainerCPU: 4000, ContainerMemory: 8192, ContainerGPU: 0, }, } eventDao := &resourceSyncEventDao{} db.SetTestManager(resourceSyncTestManager{ serviceDao: serviceDao, eventDao: eventDao, }) defer db.SetTestManager(nil) mockClient := kubecli.NewMockKubevirtClient(ctrl) mockVMInterface := kubecli.NewMockVirtualMachineInterface(ctrl) syncedServiceID := "" action := &ServiceAction{ MQClient: &noopMQClient{}, kubevirtClient: mockClient, syncVirtualMachineSpecHook: func(serviceID string) error { syncedServiceID = serviceID return nil }, } action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true } action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) { return &v1.VirtualMachine{ ObjectMeta: metav1.ObjectMeta{Name: "demo-vm", Namespace: "demo-ns"}, Spec: v1.VirtualMachineSpec{ Template: &v1.VirtualMachineInstanceTemplateSpec{ Spec: v1.VirtualMachineInstanceSpec{ Domain: v1.DomainSpec{ CPU: &v1.CPU{ Sockets: 1, Cores: 4, Threads: 1, }, Memory: &v1.Memory{ Guest: quantityPtr("8Gi"), MaxGuest: quantityPtr("16Gi"), }, }, }, }, }, Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning}, }, nil } action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) { return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{ Phase: v1.Running, Conditions: []v1.VirtualMachineInstanceCondition{ {Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"}, }, }}, nil } mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMInterface) mockVMInterface.EXPECT().Restart(gomock.Any(), "demo-vm", gomock.Any()).Return(nil) newCPU := 6000 err := action.ServiceVertical(context.Background(), &workermodel.VerticalScalingTaskBody{ ServiceID: "service-vm", ContainerCPU: &newCPU, }) if err != nil { t.Fatalf("expected windows vm cpu change to fall back to sync and restart, got %v", err) } if syncedServiceID != "service-vm" { t.Fatalf("expected vm spec sync before restart, got %q", syncedServiceID) } if serviceDao.updatedService == nil || serviceDao.updatedService.ContainerCPU != 6000 { t.Fatalf("expected updated cpu persisted, got %#v", serviceDao.updatedService) } if len(eventDao.statuses) == 0 || eventDao.statuses[len(eventDao.statuses)-1] != dbmodel.EventStatusSuccess { t.Fatalf("expected success event status, got %#v", eventDao.statuses) } } // capability_id: rainbond.vm-live-update.running-memory-shrink-rejected func TestServiceVerticalVMLiveUpdateRejectsRunningVMMemoryShrink(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() serviceDao := &resourceSyncTenantServiceDao{ service: &dbmodel.TenantServices{ ServiceID: "service-vm", ServiceAlias: "service-vm", ExtendMethod: "vm", ContainerCPU: 6000, ContainerMemory: 12288, ContainerGPU: 0, }, } eventDao := &resourceSyncEventDao{} db.SetTestManager(resourceSyncTestManager{ serviceDao: serviceDao, eventDao: eventDao, }) defer db.SetTestManager(nil) syncedServiceID := "" action := &ServiceAction{ MQClient: &noopMQClient{}, kubevirtClient: kubecli.NewMockKubevirtClient(ctrl), syncVirtualMachineSpecHook: func(serviceID string) error { syncedServiceID = serviceID return nil }, } action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true } action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) { return &v1.VirtualMachine{ Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning}, }, nil } action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) { return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{ Phase: v1.Running, Conditions: []v1.VirtualMachineInstanceCondition{ {Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"}, }, }}, nil } newMemory := 8192 err := action.ServiceVertical(context.Background(), &workermodel.VerticalScalingTaskBody{ ServiceID: "service-vm", ContainerMemory: &newMemory, }) if err == nil { t.Fatal("expected running vm memory shrink to be rejected") } if err.Error() != "虚拟机内存热更新仅支持扩容,不支持缩容,请停机后再修改规格。" { t.Fatalf("expected localized rejection message, got %q", err.Error()) } if syncedServiceID != "service-vm" { t.Fatalf("expected rollback vm spec sync, got %q", syncedServiceID) } if serviceDao.service == nil || serviceDao.service.ContainerMemory != 12288 { t.Fatalf("expected memory rollback to original value, got %#v", serviceDao.service) } if len(eventDao.statuses) == 0 || eventDao.statuses[len(eventDao.statuses)-1] != dbmodel.EventStatusFailure { t.Fatalf("expected failure event status, got %#v", eventDao.statuses) } } // capability_id: rainbond.vm-live-update.memory-target-below-max-guest func TestServiceVerticalVMLiveUpdateRejectsRunningVMMemoryAtMaxGuest(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() serviceDao := &resourceSyncTenantServiceDao{ service: &dbmodel.TenantServices{ ServiceID: "service-vm", ServiceAlias: "service-vm", ExtendMethod: "vm", ContainerCPU: 4000, ContainerMemory: 8192, ContainerGPU: 0, }, } eventDao := &resourceSyncEventDao{} db.SetTestManager(resourceSyncTestManager{ serviceDao: serviceDao, eventDao: eventDao, }) defer db.SetTestManager(nil) syncedServiceID := "" action := &ServiceAction{ MQClient: &noopMQClient{}, kubevirtClient: kubecli.NewMockKubevirtClient(ctrl), syncVirtualMachineSpecHook: func(serviceID string) error { syncedServiceID = serviceID return nil }, } action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true } action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) { return &v1.VirtualMachine{ ObjectMeta: metav1.ObjectMeta{Name: "demo-vm", Namespace: "demo-ns"}, Spec: v1.VirtualMachineSpec{ Template: &v1.VirtualMachineInstanceTemplateSpec{ Spec: v1.VirtualMachineInstanceSpec{ Domain: v1.DomainSpec{ Memory: &v1.Memory{ Guest: quantityPtr("8Gi"), MaxGuest: quantityPtr("16Gi"), }, }, }, }, }, Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning}, }, nil } action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) { return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{ Phase: v1.Running, Conditions: []v1.VirtualMachineInstanceCondition{ {Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"}, }, }}, nil } newMemory := 16384 err := action.ServiceVertical(context.Background(), &workermodel.VerticalScalingTaskBody{ ServiceID: "service-vm", ContainerMemory: &newMemory, }) if err == nil { t.Fatal("expected running vm memory target at maxGuest to be rejected") } if err.Error() != "vm memory live update target must be lower than maxGuest" { t.Fatalf("expected maxGuest headroom rejection message, got %q", err.Error()) } if syncedServiceID != "service-vm" { t.Fatalf("expected rollback vm spec sync, got %q", syncedServiceID) } if serviceDao.service == nil || serviceDao.service.ContainerMemory != 8192 { t.Fatalf("expected memory rollback to original value, got %#v", serviceDao.service) } if len(eventDao.statuses) == 0 || eventDao.statuses[len(eventDao.statuses)-1] != dbmodel.EventStatusFailure { t.Fatalf("expected failure event status, got %#v", eventDao.statuses) } } func TestGetVMLiveUpdateCapabilityIgnoresRemovedNetworkFields(t *testing.T) { serviceDao := &resourceSyncTenantServiceDao{ service: &dbmodel.TenantServices{ ServiceID: "service-vm", ServiceAlias: "service-vm", ExtendMethod: "vm", ContainerCPU: 6000, ContainerMemory: 12288, }, } db.SetTestManager(resourceSyncTestManager{ serviceDao: serviceDao, eventDao: &resourceSyncEventDao{}, }) defer db.SetTestManager(nil) action := &ServiceAction{ syncVirtualMachineSpecHook: func(serviceID string) error { return nil }, } action.loadVMRuntimeSpecExtensionSetHook = func(componentID string) (map[string]string, error) { return map[string]string{"vm_network_mode": "fixed"}, nil } action.loadVMRuntimeDeviceExtensionSetHook = func(componentID string) (map[string]string, error) { return map[string]string{}, nil } action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true } action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) { return &v1.VirtualMachine{Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning}}, nil } action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) { return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{ Phase: v1.Running, Conditions: []v1.VirtualMachineInstanceCondition{ {Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"}, }, }}, nil } capability := action.GetVMLiveUpdateCapability("service-vm") if !capability.CPUHotUpdateSupported || !capability.MemoryHotUpdateSupported { t.Fatalf("expected removed vm network fields to no longer block hot update, got %#v", capability) } if capability.HotUpdateReason != "" { t.Fatalf("did not expect removed vm network fields to set hot update reason, got %#v", capability) } } func TestGetVMLiveUpdateCapabilityRejectsFixedPodIPVM(t *testing.T) { serviceDao := &resourceSyncTenantServiceDao{ service: &dbmodel.TenantServices{ ServiceID: "service-vm", ServiceAlias: "service-vm", ExtendMethod: "vm", ContainerCPU: 6000, ContainerMemory: 12288, }, } db.SetTestManager(resourceSyncTestManager{ serviceDao: serviceDao, eventDao: &resourceSyncEventDao{}, }) defer db.SetTestManager(nil) action := &ServiceAction{} action.loadVMRuntimeSpecExtensionSetHook = func(componentID string) (map[string]string, error) { return map[string]string{ "vm_fixed_ip_enabled": "true", "vm_fixed_ip": "10.42.247.130", }, nil } action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true } capability := action.GetVMLiveUpdateCapability("service-vm") if capability.CPUHotUpdateSupported || capability.MemoryHotUpdateSupported { t.Fatalf("expected fixed IP VM hot update to be unsupported, got %#v", capability) } if !strings.Contains(capability.HotUpdateReason, "固定 IP") || !strings.Contains(capability.HotUpdateReason, "重启") { t.Fatalf("expected fixed IP restart reason, got %#v", capability) } } func TestGetVMLiveUpdateCapabilityRejectsGPUVM(t *testing.T) { serviceDao := &resourceSyncTenantServiceDao{ service: &dbmodel.TenantServices{ ServiceID: "service-vm", ServiceAlias: "service-vm", ExtendMethod: "vm", ContainerCPU: 6000, ContainerMemory: 12288, }, } db.SetTestManager(resourceSyncTestManager{ serviceDao: serviceDao, eventDao: &resourceSyncEventDao{}, }) defer db.SetTestManager(nil) action := &ServiceAction{} action.loadVMRuntimeSpecExtensionSetHook = func(componentID string) (map[string]string, error) { return map[string]string{}, nil } action.loadVMRuntimeDeviceExtensionSetHook = func(componentID string) (map[string]string, error) { return map[string]string{"vm_gpu_enabled": "true"}, nil } action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true } action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) { return &v1.VirtualMachine{Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning}}, nil } action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) { return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{ Phase: v1.Running, Conditions: []v1.VirtualMachineInstanceCondition{ {Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"}, }, }}, nil } capability := action.GetVMLiveUpdateCapability("service-vm") if capability.CPUHotUpdateSupported || capability.MemoryHotUpdateSupported { t.Fatalf("expected gpu vm to be unsupported, got %#v", capability) } if capability.HotUpdateReason == "" { t.Fatalf("expected gpu reason, got %#v", capability) } } func TestGetVMLiveUpdateCapabilityRejectsNonSocketCPUHotUpdate(t *testing.T) { serviceDao := &resourceSyncTenantServiceDao{ service: &dbmodel.TenantServices{ ServiceID: "service-vm", ServiceAlias: "service-vm", ExtendMethod: "vm", ContainerCPU: 4000, ContainerMemory: 8192, }, } db.SetTestManager(resourceSyncTestManager{ serviceDao: serviceDao, eventDao: &resourceSyncEventDao{}, }) defer db.SetTestManager(nil) action := &ServiceAction{} action.loadVMRuntimeSpecExtensionSetHook = func(componentID string) (map[string]string, error) { return map[string]string{}, nil } action.loadVMRuntimeDeviceExtensionSetHook = func(componentID string) (map[string]string, error) { return map[string]string{}, nil } action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true } action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) { return &v1.VirtualMachine{ Spec: v1.VirtualMachineSpec{ Template: &v1.VirtualMachineInstanceTemplateSpec{ Spec: v1.VirtualMachineInstanceSpec{ Domain: v1.DomainSpec{ CPU: &v1.CPU{ Sockets: 1, Cores: 4, Threads: 1, }, }, }, }, }, Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning}, }, nil } action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) { return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{ Phase: v1.Running, Conditions: []v1.VirtualMachineInstanceCondition{ {Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"}, }, }}, nil } capability := action.GetVMLiveUpdateCapability("service-vm") if capability.CPUHotUpdateSupported { t.Fatalf("expected non-socket vm cpu hot update to be disabled, got %#v", capability) } if !capability.MemoryHotUpdateSupported { t.Fatalf("expected non-socket vm memory hot update to remain enabled, got %#v", capability) } if capability.HotUpdateReason == "" { t.Fatalf("expected non-socket cpu hot update rejection reason, got %#v", capability) } } // capability_id: rainbond.vm-live-update.capability-requires-installer-media-removal func TestGetVMLiveUpdateCapabilityRejectsWhenInstallerMediaStillAttached(t *testing.T) { serviceDao := &resourceSyncTenantServiceDao{ service: &dbmodel.TenantServices{ ServiceID: "service-vm", ServiceAlias: "service-vm", ExtendMethod: "vm", ContainerCPU: 6000, ContainerMemory: 12288, }, } db.SetTestManager(resourceSyncTestManager{ serviceDao: serviceDao, eventDao: &resourceSyncEventDao{}, }) defer db.SetTestManager(nil) action := &ServiceAction{} action.loadVMRuntimeSpecExtensionSetHook = func(componentID string) (map[string]string, error) { return map[string]string{}, nil } action.loadVMRuntimeDeviceExtensionSetHook = func(componentID string) (map[string]string, error) { return map[string]string{}, nil } action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true } action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) { return &v1.VirtualMachine{ Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning}, Spec: v1.VirtualMachineSpec{ Template: &v1.VirtualMachineInstanceTemplateSpec{ Spec: v1.VirtualMachineInstanceSpec{ Domain: v1.DomainSpec{ Devices: v1.Devices{ Disks: []v1.Disk{ { Name: "vmimage", DiskDevice: v1.DiskDevice{ CDRom: &v1.CDRomTarget{Bus: v1.DiskBusSATA}, }, }, }, }, }, }, }, }, }, nil } action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) { return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{ Phase: v1.Running, Conditions: []v1.VirtualMachineInstanceCondition{ {Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"}, }, }}, nil } capability := action.GetVMLiveUpdateCapability("service-vm") if capability.CPUHotUpdateSupported || capability.MemoryHotUpdateSupported { t.Fatalf("expected installer media to block live update capability, got %#v", capability) } if !strings.Contains(capability.HotUpdateReason, "初始化安装光盘") { t.Fatalf("expected installer media reason, got %#v", capability) } } // capability_id: rainbond.vm-live-update.installer-media-removal-required func TestServiceVerticalVMLiveUpdateRejectsWhenInstallerMediaStillAttached(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() serviceDao := &resourceSyncTenantServiceDao{ service: &dbmodel.TenantServices{ ServiceID: "service-vm", ServiceAlias: "service-vm", ExtendMethod: "vm", ContainerCPU: 4000, ContainerMemory: 8192, ContainerGPU: 0, }, } eventDao := &resourceSyncEventDao{} db.SetTestManager(resourceSyncTestManager{ serviceDao: serviceDao, eventDao: eventDao, }) defer db.SetTestManager(nil) syncedServiceID := "" action := &ServiceAction{ MQClient: &noopMQClient{}, kubevirtClient: kubecli.NewMockKubevirtClient(ctrl), syncVirtualMachineSpecHook: func(serviceID string) error { syncedServiceID = serviceID return nil }, } action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true } action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) { return &v1.VirtualMachine{ Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning}, Spec: v1.VirtualMachineSpec{ Template: &v1.VirtualMachineInstanceTemplateSpec{ Spec: v1.VirtualMachineInstanceSpec{ Domain: v1.DomainSpec{ Devices: v1.Devices{ Disks: []v1.Disk{ { Name: "vmimage", DiskDevice: v1.DiskDevice{ CDRom: &v1.CDRomTarget{Bus: v1.DiskBusSATA}, }, }, }, }, }, }, }, }, }, nil } action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) { return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{ Phase: v1.Running, Conditions: []v1.VirtualMachineInstanceCondition{ {Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"}, }, }}, nil } newMemory := 10240 err := action.ServiceVertical(context.Background(), &workermodel.VerticalScalingTaskBody{ ServiceID: "service-vm", ContainerMemory: &newMemory, }) if err == nil { t.Fatal("expected live update to be rejected when installer media is still attached") } if !strings.Contains(err.Error(), "初始化安装光盘") { t.Fatalf("expected installer media rejection, got %q", err.Error()) } if syncedServiceID != "service-vm" { t.Fatalf("expected rollback vm spec sync, got %q", syncedServiceID) } if serviceDao.service == nil || serviceDao.service.ContainerMemory != 8192 { t.Fatalf("expected memory rollback to original value, got %#v", serviceDao.service) } if len(eventDao.statuses) == 0 || eventDao.statuses[len(eventDao.statuses)-1] != dbmodel.EventStatusFailure { t.Fatalf("expected failure event status, got %#v", eventDao.statuses) } } // capability_id: rainbond.vm-live-update.migration-target-missing-auto-restart func TestServiceVerticalVMLiveUpdateRestartsWhenNoMigrationTargetNode(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() serviceDao := &resourceSyncTenantServiceDao{ service: &dbmodel.TenantServices{ ServiceID: "service-vm", ServiceAlias: "service-vm", ExtendMethod: "vm", ContainerCPU: 4000, ContainerMemory: 8192, ContainerGPU: 0, }, } eventDao := &resourceSyncEventDao{} db.SetTestManager(resourceSyncTestManager{ serviceDao: serviceDao, eventDao: eventDao, }) defer db.SetTestManager(nil) kubeClient := kubefake.NewSimpleClientset( &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "virt-launcher-service-vm", Namespace: "default", Labels: map[string]string{ "service_id": "service-vm", "kubevirt.io": "virt-launcher", }, }, Spec: corev1.PodSpec{ NodeName: "node-a", NodeSelector: map[string]string{ "kubevirt.io/schedulable": "true", "cpu-vendor.node.kubevirt.io/Intel": "true", }, }, Status: corev1.PodStatus{Phase: corev1.PodRunning}, }, &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "node-a", Labels: map[string]string{ "kubevirt.io/schedulable": "true", "cpu-vendor.node.kubevirt.io/Intel": "true", }, }, }, &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "node-b", Labels: map[string]string{ "kubevirt.io/schedulable": "true", }, }, }, ) syncedServiceID := "" mockClient := kubecli.NewMockKubevirtClient(ctrl) action := &ServiceAction{ MQClient: &noopMQClient{}, kubeClient: kubeClient, kubevirtClient: mockClient, syncVirtualMachineSpecHook: func(serviceID string) error { syncedServiceID = serviceID return nil }, } action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true } action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) { return &v1.VirtualMachine{ ObjectMeta: metav1.ObjectMeta{Name: "demo-vm", Namespace: "default"}, Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning}, }, nil } action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) { return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{ Phase: v1.Running, Conditions: []v1.VirtualMachineInstanceCondition{ {Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"}, }, }}, nil } mockVMInterface := kubecli.NewMockVirtualMachineInterface(ctrl) mockClient.EXPECT().VirtualMachine("default").Return(mockVMInterface) mockVMInterface.EXPECT().Restart(gomock.Any(), "demo-vm", gomock.Any()).Return(nil) newMemory := 10240 err := action.ServiceVertical(context.Background(), &workermodel.VerticalScalingTaskBody{ ServiceID: "service-vm", ContainerMemory: &newMemory, }) if err != nil { t.Fatalf("expected missing migration target to restart instead of failing live update, got %v", err) } if syncedServiceID != "service-vm" { t.Fatalf("expected vm spec sync before restart, got %q", syncedServiceID) } if serviceDao.service == nil || serviceDao.service.ContainerMemory != 10240 { t.Fatalf("expected updated memory to persist, got %#v", serviceDao.service) } if len(eventDao.statuses) == 0 || eventDao.statuses[len(eventDao.statuses)-1] != dbmodel.EventStatusSuccess { t.Fatalf("expected success event status, got %#v", eventDao.statuses) } } // capability_id: rainbond.vm-live-update.capability-requires-migration-target func TestGetVMLiveUpdateCapabilityRejectsWhenNoMigrationTargetNode(t *testing.T) { serviceDao := &resourceSyncTenantServiceDao{ service: &dbmodel.TenantServices{ ServiceID: "service-vm", ServiceAlias: "service-vm", ExtendMethod: "vm", ContainerCPU: 6000, ContainerMemory: 12288, }, } db.SetTestManager(resourceSyncTestManager{ serviceDao: serviceDao, eventDao: &resourceSyncEventDao{}, }) defer db.SetTestManager(nil) kubeClient := kubefake.NewSimpleClientset( &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "virt-launcher-service-vm", Namespace: "default", Labels: map[string]string{ "service_id": "service-vm", "kubevirt.io": "virt-launcher", }, }, Spec: corev1.PodSpec{ NodeName: "node-a", NodeSelector: map[string]string{ "kubevirt.io/schedulable": "true", "cpu-vendor.node.kubevirt.io/Intel": "true", }, }, Status: corev1.PodStatus{Phase: corev1.PodRunning}, }, &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "node-a", Labels: map[string]string{ "kubevirt.io/schedulable": "true", "cpu-vendor.node.kubevirt.io/Intel": "true", }, }, }, &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "node-b", Labels: map[string]string{ "kubevirt.io/schedulable": "true", }, }, }, ) action := &ServiceAction{ kubeClient: kubeClient, } action.loadVMRuntimeSpecExtensionSetHook = func(componentID string) (map[string]string, error) { return map[string]string{}, nil } action.loadVMRuntimeDeviceExtensionSetHook = func(componentID string) (map[string]string, error) { return map[string]string{}, nil } action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true } action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) { return &v1.VirtualMachine{ ObjectMeta: metav1.ObjectMeta{Name: "demo-vm", Namespace: "default"}, Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning}, }, nil } action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) { return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{ Phase: v1.Running, Conditions: []v1.VirtualMachineInstanceCondition{ {Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"}, }, }}, nil } capability := action.GetVMLiveUpdateCapability("service-vm") if capability.CPUHotUpdateSupported || capability.MemoryHotUpdateSupported { t.Fatalf("expected live update capability to be blocked, got %#v", capability) } if !strings.Contains(capability.HotUpdateReason, "没有可用的迁移目标节点") { t.Fatalf("expected migration target reason, got %#v", capability) } } func quantityPtr(raw string) *resource.Quantity { q := resource.MustParse(raw) return &q }