From c7c7eea689dd5f0ea65b278ed5814bb003fb07ab Mon Sep 17 00:00:00 2001 From: haijianyang Date: Thu, 20 Jul 2023 03:07:17 -0400 Subject: [PATCH] Refactor available hosts and vm migration code --- controllers/elfmachine_controller.go | 15 +- .../elfmachine_controller_placement_group.go | 187 ++++++++---------- controllers/elfmachine_controller_test.go | 121 +++++------- controllers/vm_limiter.go | 28 --- controllers/vm_limiter_test.go | 21 -- pkg/service/collections.go | 147 ++++++++++++++ pkg/service/collections_test.go | 69 +++++++ pkg/service/mock_services/vm_mock.go | 15 +- pkg/service/util.go | 86 +------- pkg/service/util_test.go | 18 -- pkg/service/vm.go | 6 +- pkg/util/kcp/kcp.go | 16 +- 12 files changed, 382 insertions(+), 347 deletions(-) create mode 100644 pkg/service/collections.go create mode 100644 pkg/service/collections_test.go diff --git a/controllers/elfmachine_controller.go b/controllers/elfmachine_controller.go index 918beecd..4c30ba15 100644 --- a/controllers/elfmachine_controller.go +++ b/controllers/elfmachine_controller.go @@ -830,12 +830,6 @@ func (r *ElfMachineReconciler) reconcileVMTask(ctx *context.MachineContext, vm * switch { case service.IsCloneVMTask(task): releaseTicketForCreateVM(ctx.ElfMachine.Name) - case service.IsVMMigrationTask(task): - placementGroupName, err := towerresources.GetVMPlacementGroupName(ctx, ctx.Client, ctx.Machine, ctx.Cluster) - if err != nil { - return false, err - } - releaseTicketForPlacementGroupVMMigration(placementGroupName) case service.IsMemoryInsufficientError(errorMessage): setElfClusterMemoryInsufficient(ctx.ElfCluster.Spec.Cluster, true) message := fmt.Sprintf("Insufficient memory detected for ELF cluster %s", ctx.ElfCluster.Spec.Cluster) @@ -846,16 +840,9 @@ func (r *ElfMachineReconciler) reconcileVMTask(ctx *context.MachineContext, vm * case models.TaskStatusSUCCESSED: ctx.Logger.Info("VM task succeeded", "vmRef", vmRef, "taskRef", taskRef, "taskDescription", service.GetTowerString(task.Description)) - switch { - case service.IsCloneVMTask(task) || service.IsPowerOnVMTask(task): + if service.IsCloneVMTask(task) || service.IsPowerOnVMTask(task) { setElfClusterMemoryInsufficient(ctx.ElfCluster.Spec.Cluster, false) releaseTicketForCreateVM(ctx.ElfMachine.Name) - case service.IsVMMigrationTask(task): - placementGroupName, err := towerresources.GetVMPlacementGroupName(ctx, ctx.Client, ctx.Machine, ctx.Cluster) - if err != nil { - return false, err - } - releaseTicketForPlacementGroupVMMigration(placementGroupName) } default: ctx.Logger.Info("Waiting for VM task done", "vmRef", vmRef, "taskRef", taskRef, "taskStatus", service.GetTowerTaskStatus(task.Status), "taskDescription", service.GetTowerString(task.Description)) diff --git a/controllers/elfmachine_controller_placement_group.go b/controllers/elfmachine_controller_placement_group.go index 2e27f4ae..5f249a3b 100644 --- a/controllers/elfmachine_controller_placement_group.go +++ b/controllers/elfmachine_controller_placement_group.go @@ -145,7 +145,7 @@ func (r *ElfMachineReconciler) preCheckPlacementGroup(ctx *context.MachineContex return nil, err } - usedHostsByPG, err := r.getHostsInPlacementGroup(ctx, placementGroup) + usedHostSetByPG, err := r.getHostsInPlacementGroup(ctx, placementGroup) if err != nil { return nil, err } @@ -155,10 +155,10 @@ func (r *ElfMachineReconciler) preCheckPlacementGroup(ctx *context.MachineContex return nil, err } + usedHostsByPG := hosts.Find(usedHostSetByPG) availableHosts := r.getAvailableHostsForVM(ctx, hosts, usedHostsByPG, nil) - availableHostSet := service.HostsToSet(availableHosts) - if availableHostSet.Len() != 0 { - ctx.Logger.V(1).Info("The placement group still has capacity", "placementGroup", *placementGroup.Name, "availableHosts", availableHostSet.UnsortedList()) + if !availableHosts.IsEmpty() { + ctx.Logger.V(1).Info("The placement group still has capacity", "placementGroup", *placementGroup.Name, "availableHosts", availableHosts.String()) return pointer.String(""), nil } @@ -170,7 +170,7 @@ func (r *ElfMachineReconciler) preCheckPlacementGroup(ctx *context.MachineContex // KCP is not in scaling down/rolling update. if !(kcputil.IsKCPRollingUpdateFirstMachine(kcp) || kcputil.IsKCPInScalingDown(kcp)) { - ctx.Logger.V(1).Info("The placement group is full, wait for enough available hosts", "placementGroup", *placementGroup.Name, "availableHosts", availableHostSet.UnsortedList(), "usedHostsByPG", usedHostsByPG.UnsortedList()) + ctx.Logger.V(1).Info("The placement group is full, wait for enough available hosts", "placementGroup", *placementGroup.Name, "availableHosts", availableHosts.String(), "usedHostsByPG", usedHostsByPG.String()) return nil, nil } @@ -192,7 +192,7 @@ func (r *ElfMachineReconciler) preCheckPlacementGroup(ctx *context.MachineContex return nil, errors.Wrapf(err, "failed to init patch helper for %s %s/%s", ctx.Machine.GroupVersionKind(), ctx.Machine.Namespace, ctx.Machine.Name) } - ctx.Logger.Info("Add the delete machine annotation on KCP Machine in order to delete it, because KCP is being scaled down after a failed scaling up", "placementGroup", *placementGroup.Name, "availableHosts", availableHostSet.UnsortedList()) + ctx.Logger.Info("Add the delete machine annotation on KCP Machine in order to delete it, because KCP is being scaled down after a failed scaling up", "placementGroup", *placementGroup.Name, "availableHosts", availableHosts.String()) // Allow scaling down of KCP with the possibility of marking specific control plane machine(s) to be deleted with delete annotation key. // The presence of the annotation will affect the rollout strategy in a way that, it implements the following prioritization logic in descending order, @@ -213,7 +213,7 @@ func (r *ElfMachineReconciler) preCheckPlacementGroup(ctx *context.MachineContex // KCP is in rolling update. - if !service.ContainsUnavailableHost(hosts, usedHostsByPG.UnsortedList(), *service.TowerMemory(ctx.ElfMachine.Spec.MemoryMiB)) && + if usedHostsByPG.Len() == usedHostsByPG.Available(*service.TowerMemory(ctx.ElfMachine.Spec.MemoryMiB)).Len() && int(*kcp.Spec.Replicas) == usedHostsByPG.Len() { // Only when KCP is in rolling update and the placement group is full // does it need to get the latest created machine. @@ -225,7 +225,7 @@ func (r *ElfMachineReconciler) preCheckPlacementGroup(ctx *context.MachineContex return pointer.String(hostID), err } - ctx.Logger.V(1).Info("The placement group is full, wait for enough available hosts", "placementGroup", *placementGroup.Name, "availableHosts", availableHostSet.UnsortedList(), "usedHostsByPG", usedHostsByPG.UnsortedList()) + ctx.Logger.V(1).Info("The placement group is full, wait for enough available hosts", "placementGroup", *placementGroup.Name, "availableHosts", availableHosts.String(), "usedHostsByPG", usedHostsByPG.String()) return nil, nil } @@ -235,7 +235,7 @@ func (r *ElfMachineReconciler) preCheckPlacementGroup(ctx *context.MachineContex // Find the latest created machine in the placement group, // and set the host where the machine is located to the first machine created by KCP rolling update. // This prevents migration of virtual machine during KCP rolling update when using a placement group. -func (r *ElfMachineReconciler) getVMHostForRollingUpdate(ctx *context.MachineContext, placementGroup *models.VMPlacementGroup, hosts []*models.Host) (string, error) { +func (r *ElfMachineReconciler) getVMHostForRollingUpdate(ctx *context.MachineContext, placementGroup *models.VMPlacementGroup, hosts service.Hosts) (string, error) { elfMachines, err := machineutil.GetControlPlaneElfMachinesInCluster(ctx, ctx.Client, ctx.Cluster.Namespace, ctx.Cluster.Name) if err != nil { return "", err @@ -267,7 +267,7 @@ func (r *ElfMachineReconciler) getVMHostForRollingUpdate(ctx *context.MachineCon if vm, err := ctx.VMService.Get(vmMap[machine.Name]); err != nil { return "", err } else { - host := service.GetHostFromList(*vm.Host.ID, hosts) + host := hosts.Get(*vm.Host.ID) if host == nil { ctx.Logger.Info("Host not found, skip selecting host for VM", "hostID", *vm.Host.ID, "vmRef", ctx.ElfMachine.Status.VMRef) } else { @@ -307,33 +307,22 @@ func (r *ElfMachineReconciler) getHostsInPlacementGroup(ctx *context.MachineCont // The 'Available' means that the specified VM can run on these hosts. // It returns hosts that are not in faulted state, not in the given 'usedHostsByPG', // and have sufficient memory for running this VM. -func (r *ElfMachineReconciler) getAvailableHostsForVM(ctx *context.MachineContext, hosts []*models.Host, usedHostsByPG sets.Set[string], vm *models.VM) []*models.Host { +func (r *ElfMachineReconciler) getAvailableHostsForVM(ctx *context.MachineContext, hosts service.Hosts, usedHostsByPG service.Hosts, vm *models.VM) service.Hosts { + availableHosts := hosts.Available(*service.TowerMemory(ctx.ElfMachine.Spec.MemoryMiB)).Difference(usedHostsByPG) + // If the VM is running, and the host where the VM is located // is not used by the placement group, then it is not necessary to // check the memory is sufficient to determine whether the host is available. - // Otherwise, the VM may need to be migrated to another host, - // and need to check whether the memory is sufficient. - if vm != nil && (*vm.Status == models.VMStatusRUNNING || *vm.Status == models.VMStatusSUSPENDED) { - availableHosts := service.GetAvailableHosts(hosts, 0) - unusedHostsByPG := service.HostsToSet(availableHosts).Difference(usedHostsByPG) - - if unusedHostsByPG.Has(*vm.Host.ID) { - return service.FilterHosts(availableHosts, usedHostsByPG) + if vm != nil && + (*vm.Status == models.VMStatusRUNNING || *vm.Status == models.VMStatusSUSPENDED) && + !availableHosts.Contains(*vm.Host.ID) { + host := hosts.Get(*vm.Host.ID) + available, _ := service.IsAvailableHost(host, 0) + if available && !usedHostsByPG.Contains(*host.ID) { + availableHosts.Insert(host) } - - // The virtual machine is not on an unused host and may need to be migrated, - // so need to check whether the host memory is sufficient. - unusedHosts := service.FilterHosts(availableHosts, usedHostsByPG) - availableHosts = service.GetAvailableHosts(unusedHosts, *service.TowerMemory(ctx.ElfMachine.Spec.MemoryMiB)) - - return availableHosts } - // The virtual machine has not been created or is not powered on. - // Need to check whether the memory of the host is sufficient. - availableHosts := service.GetAvailableHosts(hosts, *service.TowerMemory(ctx.ElfMachine.Spec.MemoryMiB)) - availableHosts = service.FilterHosts(availableHosts, usedHostsByPG) - return availableHosts } @@ -360,6 +349,8 @@ func (r *ElfMachineReconciler) getPlacementGroup(ctx *context.MachineContext, pl // For example, the virtual machine has joined the placement group. // 2. false and error is nil means the virtual machine has not joined the placement group. // For example, the placement group is full or the virtual machine is being migrated. +// +//nolint:gocyclo func (r *ElfMachineReconciler) joinPlacementGroup(ctx *context.MachineContext, vm *models.VM) (ret bool, reterr error) { if !version.IsCompatibleWithPlacementGroup(ctx.ElfMachine) { ctx.Logger.V(1).Info(fmt.Sprintf("The capeVersion of ElfMachine is lower than %s, skip adding VM to the placement group", version.CAPEVersion1_2_0), "capeVersion", version.GetCAPEVersion(ctx.ElfMachine)) @@ -401,19 +392,20 @@ func (r *ElfMachineReconciler) joinPlacementGroup(ctx *context.MachineContext, v } if machineutil.IsControlPlaneMachine(ctx.Machine) { - usedHostsByPG, err := r.getHostsInPlacementGroup(ctx, placementGroup) + hosts, err := ctx.VMService.GetHostsByCluster(ctx.ElfCluster.Spec.Cluster) if err != nil { return false, err } - hosts, err := ctx.VMService.GetHostsByCluster(ctx.ElfCluster.Spec.Cluster) + usedHostSetByPG, err := r.getHostsInPlacementGroup(ctx, placementGroup) if err != nil { return false, err } + usedHostsByPG := hosts.Find(usedHostSetByPG) + availableHosts := r.getAvailableHostsForVM(ctx, hosts, usedHostsByPG, vm) - availableHostSet := service.HostsToSet(availableHosts) - if availableHostSet.Len() == 0 { + if availableHosts.IsEmpty() { kcp, err := machineutil.GetKCPByMachine(ctx, ctx.Client, ctx.Machine) if err != nil { return false, err @@ -424,21 +416,21 @@ func (r *ElfMachineReconciler) joinPlacementGroup(ctx *context.MachineContext, v // In this case first machine created by KCP rolling update can be powered on without being added to the placement group. if kcputil.IsKCPRollingUpdateFirstMachine(kcp) && *vm.Status == models.VMStatusSTOPPED && - !service.ContainsUnavailableHost(hosts, usedHostsByPG.UnsortedList(), *service.TowerMemory(ctx.ElfMachine.Spec.MemoryMiB)) && + usedHostsByPG.Len() == usedHostsByPG.Available(*service.TowerMemory(ctx.ElfMachine.Spec.MemoryMiB)).Len() && int(*kcp.Spec.Replicas) == usedHostsByPG.Len() { - ctx.Logger.Info("The placement group is full and KCP is in rolling update, skip adding VM to the placement group", "placementGroup", *placementGroup.Name, "availableHosts", availableHostSet.UnsortedList(), "usedHostsByPG", usedHostsByPG.UnsortedList(), "vmRef", ctx.ElfMachine.Status.VMRef, "vmId", *vm.ID) + ctx.Logger.Info("The placement group is full and KCP is in rolling update, skip adding VM to the placement group", "placementGroup", *placementGroup.Name, "availableHosts", availableHosts.String(), "usedHostsByPG", usedHostsByPG.String(), "vmRef", ctx.ElfMachine.Status.VMRef, "vmId", *vm.ID) return true, nil } if *vm.Status == models.VMStatusRUNNING || *vm.Status == models.VMStatusSUSPENDED { - ctx.Logger.V(1).Info(fmt.Sprintf("The placement group is full and VM is in %s status, skip adding VM to the placement group", *vm.Status), "placementGroup", *placementGroup.Name, "availableHosts", availableHostSet.UnsortedList(), "usedHostsByPG", usedHostsByPG.UnsortedList(), "vmRef", ctx.ElfMachine.Status.VMRef, "vmId", *vm.ID) + ctx.Logger.V(1).Info(fmt.Sprintf("The placement group is full and VM is in %s status, skip adding VM to the placement group", *vm.Status), "placementGroup", *placementGroup.Name, "availableHosts", availableHosts.String(), "usedHostsByPG", usedHostsByPG.String(), "vmRef", ctx.ElfMachine.Status.VMRef, "vmId", *vm.ID) return true, nil } // KCP is scaling out or being created. - ctx.Logger.V(1).Info("The placement group is full, wait for enough available hosts", "placementGroup", *placementGroup.Name, "availableHosts", availableHostSet.UnsortedList(), "usedHostsByPG", usedHostsByPG.UnsortedList(), "vmRef", ctx.ElfMachine.Status.VMRef, "vmId", *vm.ID) + ctx.Logger.V(1).Info("The placement group is full, wait for enough available hosts", "placementGroup", *placementGroup.Name, "availableHosts", availableHosts.String(), "usedHostsByPG", usedHostsByPG.String(), "vmRef", ctx.ElfMachine.Status.VMRef, "vmId", *vm.ID) return false, nil } @@ -447,9 +439,56 @@ func (r *ElfMachineReconciler) joinPlacementGroup(ctx *context.MachineContext, v // and the virtual machine is not STOPPED, we need to migrate the virtual machine to a host that // is not used by the placement group before adding the virtual machine to the placement group. // Otherwise, just add the virtual machine to the placement group directly. - ctx.Logger.V(1).Info("The availableHosts for migrating the VM", "hosts", availableHostSet, "vmHost", *vm.Host.ID) - if !availableHostSet.Has(*vm.Host.ID) && *vm.Status != models.VMStatusSTOPPED { - return r.migrateVMForJoiningPlacementGroup(ctx, vm, placementGroup, availableHostSet.UnsortedList()[0]) + ctx.Logger.V(1).Info("The availableHosts for migrating the VM", "hosts", availableHosts.String(), "vmHost", *vm.Host.ID) + if !availableHosts.Contains(*vm.Host.ID) && *vm.Status != models.VMStatusSTOPPED { + ctx.Logger.V(1).Info("Try to migrate the virtual machine to the specified target host if needed") + + kcp, err := machineutil.GetKCPByMachine(ctx, ctx.Client, ctx.Machine) + if err != nil { + return false, err + } + + if kcputil.IsKCPRollingUpdate(kcp) { + ctx.Logger.Info("KCP rolling update in progress, skip migrating VM", "vmRef", ctx.ElfMachine.Status.VMRef, "vmId", *vm.ID) + return true, nil + } + + // The 1st new CP ElfMachine should wait for other new CP ElfMachines to join the target PlacementGroup. + // The code below double checks the recommended target host for migration is valid. + cpElfMachines, err := machineutil.GetControlPlaneElfMachinesInCluster(ctx, ctx.Client, ctx.Cluster.Namespace, ctx.Cluster.Name) + if err != nil { + return false, err + } + + targetHost := availableHosts.UnsortedList()[0] + usedHostsByPG := sets.Set[string]{} + for i := 0; i < len(cpElfMachines); i++ { + if ctx.ElfMachine.Name != cpElfMachines[i].Name && + cpElfMachines[i].Status.PlacementGroupRef == *placementGroup.ID && + cpElfMachines[i].CreationTimestamp.After(ctx.ElfMachine.CreationTimestamp.Time) { + usedHostsByPG.Insert(cpElfMachines[i].Status.HostServerRef) + } + } + + usedHostsCount := usedHostsByPG.Len() + ctx.Logger.V(1).Info("The hosts used by the PlacementGroup", "usedHosts", usedHostsByPG, "count", usedHostsCount, "targetHost", targetHost) + if usedHostsCount < int(*kcp.Spec.Replicas-1) { + ctx.Logger.V(1).Info("Not all other CPs joined the PlacementGroup, skip migrating VM") + return true, nil + } + + if usedHostsByPG.Has(*targetHost.ID) { + ctx.Logger.V(1).Info("The recommended target host for VM migration is used by the PlacementGroup, skip migrating VM") + return true, nil + } + + // KCP is not in rolling update process. + // This is the last CP ElfMachine (i.e. the 1st new CP ElfMachine) which has not been added into the target PlacementGroup. + // Migrate this VM to the target host, then it will be added into the target PlacementGroup. + + ctx.Logger.V(1).Info("Start migrateVM since KCP is not in rolling update process", "targetHost", targetHost) + + return r.migrateVM(ctx, vm, *targetHost.ID) } } @@ -463,79 +502,17 @@ func (r *ElfMachineReconciler) joinPlacementGroup(ctx *context.MachineContext, v return true, nil } -// migrateVMForJoiningPlacementGroup migrates the virtual machine to the specified target host -// for joining placement group. -// -// The return value: -// 1. true means that the virtual machine does not need to be migrated. -// 2. false and error is nil means the virtual machine is being migrated. -func (r *ElfMachineReconciler) migrateVMForJoiningPlacementGroup(ctx *context.MachineContext, vm *models.VM, placementGroup *models.VMPlacementGroup, targetHost string) (bool, error) { - ctx.Logger.V(1).Info("Try to migrate the virtual machine to the specified target host if needed") - kcp, err := machineutil.GetKCPByMachine(ctx, ctx.Client, ctx.Machine) - if err != nil { - return false, err - } - - // When *kcp.Spec.Replicas != kcp.Status.UpdatedReplicas, it must be in a KCP rolling update process. - // When *kcp.Spec.Replicas == kcp.Status.UpdatedReplicas, it could be in one of the following cases: - // 1) It's not in a KCP rolling update process. So kcp.Spec.Replicas == kcp.Status.Replicas. - // 2) It's at the end of a KCP rolling update process, and the last KCP replica (i.e the last KCP ElfMachine) is created just now. - // There is still an old KCP ElfMachine, so kcp.Spec.Replicas + 1 == kcp.Status.Replicas. - - if *kcp.Spec.Replicas != kcp.Status.UpdatedReplicas || *kcp.Spec.Replicas != kcp.Status.Replicas { - ctx.Logger.Info("KCP rolling update in progress, skip migrating VM", "vmRef", ctx.ElfMachine.Status.VMRef, "vmId", *vm.ID) - return true, nil - } - - // The 1st new CP ElfMachine should wait for other new CP ElfMachines to join the target PlacementGroup. - // The code below double checks the recommended target host for migration is valid. - cpElfMachines, err := machineutil.GetControlPlaneElfMachinesInCluster(ctx, ctx.Client, ctx.Cluster.Namespace, ctx.Cluster.Name) - if err != nil { - return false, err - } - usedHostsByPG := sets.Set[string]{} - for i := 0; i < len(cpElfMachines); i++ { - if ctx.ElfMachine.Name != cpElfMachines[i].Name && - cpElfMachines[i].Status.PlacementGroupRef == *placementGroup.ID && - cpElfMachines[i].CreationTimestamp.After(ctx.ElfMachine.CreationTimestamp.Time) { - usedHostsByPG.Insert(cpElfMachines[i].Status.HostServerRef) - } - } - usedHostsCount := usedHostsByPG.Len() - ctx.Logger.V(1).Info("The hosts used by the PlacementGroup", "usedHosts", usedHostsByPG, "count", usedHostsCount, "targetHost", targetHost) - if usedHostsCount < int(*kcp.Spec.Replicas-1) { - ctx.Logger.V(1).Info("Not all other CPs joined the PlacementGroup, skip migrating VM") - return true, nil - } - if usedHostsByPG.Has(targetHost) { - ctx.Logger.V(1).Info("The recommended target host for VM migration is used by the PlacementGroup, skip migrating VM") - return true, nil - } - - // KCP is not in rolling update process. - // This is the last CP ElfMachine (i.e. the 1st new CP ElfMachine) which has not been added into the target PlacementGroup. - // Migrate this VM to the target host, then it will be added into the target PlacementGroup. - ctx.Logger.V(1).Info("Start migrateVM since KCP is not in rolling update process", "targetHost", targetHost) - return r.migrateVM(ctx, vm, placementGroup, targetHost) -} - // migrateVM migrates the virtual machine to the specified target host. // // The return value: // 1. true means that the virtual machine does not need to be migrated. // 2. false and error is nil means the virtual machine is being migrated. -func (r *ElfMachineReconciler) migrateVM(ctx *context.MachineContext, vm *models.VM, placementGroup *models.VMPlacementGroup, targetHost string) (bool, error) { +func (r *ElfMachineReconciler) migrateVM(ctx *context.MachineContext, vm *models.VM, targetHost string) (bool, error) { if *vm.Host.ID == targetHost { ctx.Logger.V(1).Info(fmt.Sprintf("The VM is already on the recommended target host %s, skip migrating VM", targetHost)) return true, nil } - if ok := acquireTicketForPlacementGroupVMMigration(*placementGroup.Name); !ok { - ctx.Logger.V(1).Info("The placement group is performing another VM migration, skip migrating VM", "placementGroup", service.GetTowerString(placementGroup.Name), "vmRef", ctx.ElfMachine.Status.VMRef, "vmId", *vm.ID) - - return false, nil - } - withTaskVM, err := ctx.VMService.Migrate(service.GetTowerString(vm.ID), targetHost) if err != nil { return false, err diff --git a/controllers/elfmachine_controller_test.go b/controllers/elfmachine_controller_test.go index f108ff14..23cb50ec 100644 --- a/controllers/elfmachine_controller_test.go +++ b/controllers/elfmachine_controller_test.go @@ -35,7 +35,6 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/sets" "k8s.io/klog/v2" "k8s.io/utils/pointer" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" @@ -933,7 +932,7 @@ var _ = Describe("ElfMachineReconciler", func() { fake.InitOwnerReferences(ctrlContext, elfCluster, cluster, elfMachine, machine) mockVMService.EXPECT().GetVMPlacementGroup(gomock.Any()).Return(placementGroup, nil) - mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return([]*models.Host{host}, nil) + mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return(service.NewHosts(host), nil) mockVMService.EXPECT().FindByIDs([]string{}).Return([]*models.VM{}, nil) mockVMService.EXPECT().AddVMsToPlacementGroup(placementGroup, []string{*vm.ID}).Return(task, nil) mockVMService.EXPECT().WaitTask(*task.ID, config.WaitTaskTimeout, config.WaitTaskInterval).Return(task, nil) @@ -958,12 +957,13 @@ var _ = Describe("ElfMachineReconciler", func() { kcp.Spec.Replicas = pointer.Int32(1) kcp.Status.Replicas = 2 kcp.Status.UpdatedReplicas = 1 + conditions.MarkFalse(kcp, controlplanev1.MachinesSpecUpToDateCondition, controlplanev1.RollingUpdateInProgressReason, clusterv1.ConditionSeverityWarning, "") ctrlContext := newCtrlContexts(elfCluster, cluster, elfMachine, machine, secret, md, kcp) machineContext := newMachineContext(ctrlContext, elfCluster, cluster, elfMachine, machine, mockVMService) fake.InitOwnerReferences(ctrlContext, elfCluster, cluster, elfMachine, machine) mockVMService.EXPECT().GetVMPlacementGroup(gomock.Any()).Return(placementGroup, nil) - mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return([]*models.Host{host}, nil) + mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return(service.NewHosts(host), nil) mockVMService.EXPECT().FindByIDs([]string{*placementGroup.Vms[0].ID}).Return([]*models.VM{vm2}, nil) reconciler := &ElfMachineReconciler{ControllerContext: ctrlContext, NewVMService: mockNewVMService} @@ -976,7 +976,7 @@ var _ = Describe("ElfMachineReconciler", func() { klog.SetOutput(logBuffer) host.HostState = &models.NestedMaintenanceHostState{State: models.NewMaintenanceModeEnum(models.MaintenanceModeEnumMAINTENANCEMODE)} mockVMService.EXPECT().GetVMPlacementGroup(gomock.Any()).Return(placementGroup, nil) - mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return([]*models.Host{host}, nil) + mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return(service.NewHosts(host), nil) mockVMService.EXPECT().FindByIDs([]string{*placementGroup.Vms[0].ID}).Return([]*models.VM{}, nil) reconciler = &ElfMachineReconciler{ControllerContext: ctrlContext, NewVMService: mockNewVMService} @@ -990,7 +990,7 @@ var _ = Describe("ElfMachineReconciler", func() { vm.Status = models.NewVMStatus(models.VMStatusRUNNING) host.HostState = &models.NestedMaintenanceHostState{State: models.NewMaintenanceModeEnum(models.MaintenanceModeEnumMAINTENANCEMODE)} mockVMService.EXPECT().GetVMPlacementGroup(gomock.Any()).Return(placementGroup, nil) - mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return([]*models.Host{host}, nil) + mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return(service.NewHosts(host), nil) mockVMService.EXPECT().FindByIDs([]string{*placementGroup.Vms[0].ID}).Return([]*models.VM{}, nil) reconciler = &ElfMachineReconciler{ControllerContext: ctrlContext, NewVMService: mockNewVMService} @@ -1020,7 +1020,7 @@ var _ = Describe("ElfMachineReconciler", func() { fake.InitOwnerReferences(ctrlContext, elfCluster, cluster, elfMachine, machine) mockVMService.EXPECT().GetVMPlacementGroup(gomock.Any()).Return(placementGroup, nil) - mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return([]*models.Host{host1, host2, host3}, nil) + mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return(service.NewHosts(host1, host2, host3), nil) mockVMService.EXPECT().FindByIDs([]string{*vm2.ID}).Return([]*models.VM{vm2}, nil) mockVMService.EXPECT().AddVMsToPlacementGroup(placementGroup, gomock.Any()).Return(task, nil) mockVMService.EXPECT().WaitTask(*task.ID, config.WaitTaskTimeout, config.WaitTaskInterval).Return(task, nil) @@ -1051,12 +1051,13 @@ var _ = Describe("ElfMachineReconciler", func() { kcp.Spec.Replicas = pointer.Int32(3) kcp.Status.UpdatedReplicas = 3 kcp.Status.Replicas = 4 + conditions.MarkFalse(kcp, controlplanev1.MachinesSpecUpToDateCondition, controlplanev1.RollingUpdateInProgressReason, clusterv1.ConditionSeverityWarning, "") ctrlContext := newCtrlContexts(elfCluster, cluster, elfMachine, machine, secret, md, kcp) machineContext := newMachineContext(ctrlContext, elfCluster, cluster, elfMachine, machine, mockVMService) fake.InitOwnerReferences(ctrlContext, elfCluster, cluster, elfMachine, machine) mockVMService.EXPECT().GetVMPlacementGroup(gomock.Any()).Return(placementGroup, nil) - mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return([]*models.Host{host1, host2, host3}, nil) + mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return(service.NewHosts(host1, host2, host3), nil) mockVMService.EXPECT().FindByIDs(gomock.InAnyOrder([]string{*newCP2.ID, *oldCP3.ID})).Return([]*models.VM{newCP2, oldCP3}, nil) reconciler := &ElfMachineReconciler{ControllerContext: ctrlContext, NewVMService: mockNewVMService} @@ -1116,7 +1117,7 @@ var _ = Describe("ElfMachineReconciler", func() { mockVMService.EXPECT().GetVMPlacementGroup(gomock.Any()).Return(placementGroup, nil) mockVMService.EXPECT().FindByIDs(gomock.InAnyOrder([]string{*vm1.ID, *vm2.ID})).Return([]*models.VM{vm1, vm2}, nil) - mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return([]*models.Host{host0, host1, host2}, nil) + mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return(service.NewHosts(host0, host1, host2), nil) mockVMService.EXPECT().Migrate(*vm0.ID, *host0.ID).Return(withTaskVM, nil) reconciler := &ElfMachineReconciler{ControllerContext: ctrlContext, NewVMService: mockNewVMService} @@ -1133,7 +1134,7 @@ var _ = Describe("ElfMachineReconciler", func() { elfMachine1.Status.HostServerRef = *host0.ID mockVMService.EXPECT().GetVMPlacementGroup(gomock.Any()).Return(placementGroup, nil) mockVMService.EXPECT().FindByIDs(gomock.InAnyOrder([]string{*vm1.ID, *vm2.ID})).Return([]*models.VM{vm1, vm2}, nil) - mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return([]*models.Host{host0, host1, host2}, nil) + mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return(service.NewHosts(host0, host1, host2), nil) ctrlContext = newCtrlContexts(elfCluster, cluster, elfMachine, machine, secret, kcp, elfMachine1, machine1, elfMachine2, machine2) machineContext = newMachineContext(ctrlContext, elfCluster, cluster, elfMachine, machine, mockVMService) fake.InitOwnerReferences(ctrlContext, elfCluster, cluster, elfMachine, machine) @@ -1150,13 +1151,12 @@ var _ = Describe("ElfMachineReconciler", func() { host := fake.NewTowerHost() vm := fake.NewTowerVMFromElfMachine(elfMachine) vm.Host = &models.NestedHost{ID: service.TowerString(*host.ID)} - placementGroup := fake.NewVMPlacementGroup([]string{}) ctrlContext := newCtrlContexts(elfCluster, cluster, elfMachine, machine, secret, kcp) machineContext := newMachineContext(ctrlContext, elfCluster, cluster, elfMachine, machine, mockVMService) fake.InitOwnerReferences(ctrlContext, elfCluster, cluster, elfMachine, machine) reconciler := &ElfMachineReconciler{ControllerContext: ctrlContext, NewVMService: mockNewVMService} - ok, err := reconciler.migrateVM(machineContext, vm, placementGroup, *host.ID) + ok, err := reconciler.migrateVM(machineContext, vm, *host.ID) Expect(ok).To(BeTrue()) Expect(err).To(BeZero()) Expect(logBuffer.String()).To(ContainSubstring("The VM is already on the recommended target host")) @@ -1219,7 +1219,7 @@ var _ = Describe("ElfMachineReconciler", func() { logBuffer = new(bytes.Buffer) klog.SetOutput(logBuffer) mockVMService.EXPECT().GetVMPlacementGroup(placementGroupName).Return(placementGroup, nil) - mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return([]*models.Host{host}, nil) + mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return(service.NewHosts(host), nil) mockVMService.EXPECT().FindByIDs(gomock.InAnyOrder([]string{})).Return([]*models.VM{}, nil) reconciler := &ElfMachineReconciler{ControllerContext: ctrlContext, NewVMService: mockNewVMService} @@ -1231,7 +1231,7 @@ var _ = Describe("ElfMachineReconciler", func() { klog.SetOutput(logBuffer) placementGroup.Vms = []*models.NestedVM{{ID: vm1.ID, Name: vm1.Name}} mockVMService.EXPECT().GetVMPlacementGroup(placementGroupName).Return(placementGroup, nil) - mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return([]*models.Host{host}, nil) + mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return(service.NewHosts(host), nil) mockVMService.EXPECT().FindByIDs(gomock.InAnyOrder([]string{*vm1.ID})).Return([]*models.VM{vm1}, nil) reconciler = &ElfMachineReconciler{ControllerContext: ctrlContext, NewVMService: mockNewVMService} @@ -1241,7 +1241,7 @@ var _ = Describe("ElfMachineReconciler", func() { Expect(logBuffer.String()).To(ContainSubstring("The placement group is full, wait for enough available hosts")) }) - It("when placement group is full", func() { + It("when placement group is full and KCP rolling update in progress", func() { host1 := fake.NewTowerHost() host2 := fake.NewTowerHost() host3 := fake.NewTowerHost() @@ -1277,6 +1277,7 @@ var _ = Describe("ElfMachineReconciler", func() { kcp.Spec.Replicas = pointer.Int32(3) kcp.Status.Replicas = 4 kcp.Status.UpdatedReplicas = 1 + conditions.MarkFalse(kcp, controlplanev1.MachinesSpecUpToDateCondition, controlplanev1.RollingUpdateInProgressReason, clusterv1.ConditionSeverityWarning, "") ctrlContext := newCtrlContexts(elfCluster, cluster, elfMachine, machine, secret, kcp, elfMachine1, machine1, elfMachine2, machine2, elfMachine3, machine3) machineContext := newMachineContext(ctrlContext, elfCluster, cluster, elfMachine, machine, mockVMService) fake.InitOwnerReferences(ctrlContext, elfCluster, cluster, elfMachine, machine) @@ -1290,7 +1291,7 @@ var _ = Describe("ElfMachineReconciler", func() { klog.SetOutput(logBuffer) mockVMService.EXPECT().Get(*vm3.ID).Return(vm3, nil) mockVMService.EXPECT().GetVMPlacementGroup(placementGroupName).Return(placementGroup, nil) - mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return([]*models.Host{host1, host2, host3}, nil) + mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return(service.NewHosts(host1, host2, host3), nil) mockVMService.EXPECT().FindByIDs(gomock.InAnyOrder([]string{*vm1.ID, *vm2.ID, *vm3.ID})).Return([]*models.VM{vm1, vm2, vm3}, nil) reconciler := &ElfMachineReconciler{ControllerContext: ctrlContext, NewVMService: mockNewVMService} @@ -1304,7 +1305,7 @@ var _ = Describe("ElfMachineReconciler", func() { klog.SetOutput(logBuffer) host1.Status = models.NewHostStatus(models.HostStatusCONNECTEDERROR) mockVMService.EXPECT().GetVMPlacementGroup(placementGroupName).Return(placementGroup, nil) - mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return([]*models.Host{host1, host2, host3}, nil) + mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return(service.NewHosts(host1, host2, host3), nil) mockVMService.EXPECT().FindByIDs(gomock.InAnyOrder([]string{*vm1.ID, *vm2.ID, *vm3.ID})).Return([]*models.VM{vm1, vm2, vm3}, nil) reconciler = &ElfMachineReconciler{ControllerContext: ctrlContext, NewVMService: mockNewVMService} @@ -1318,7 +1319,7 @@ var _ = Describe("ElfMachineReconciler", func() { host1.Status = models.NewHostStatus(models.HostStatusCONNECTEDHEALTHY) host2.Status = models.NewHostStatus(models.HostStatusCONNECTEDERROR) mockVMService.EXPECT().GetVMPlacementGroup(placementGroupName).Return(placementGroup, nil) - mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return([]*models.Host{host1, host2, host3}, nil) + mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return(service.NewHosts(host1, host2, host3), nil) mockVMService.EXPECT().FindByIDs(gomock.InAnyOrder([]string{*vm1.ID, *vm2.ID, *vm3.ID})).Return([]*models.VM{vm1, vm2, vm3}, nil) reconciler = &ElfMachineReconciler{ControllerContext: ctrlContext, NewVMService: mockNewVMService} @@ -1332,7 +1333,7 @@ var _ = Describe("ElfMachineReconciler", func() { host2.Status = models.NewHostStatus(models.HostStatusCONNECTEDHEALTHY) host3.Status = models.NewHostStatus(models.HostStatusCONNECTEDERROR) mockVMService.EXPECT().GetVMPlacementGroup(placementGroupName).Return(placementGroup, nil) - mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return([]*models.Host{host1, host2, host3}, nil) + mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return(service.NewHosts(host1, host2, host3), nil) mockVMService.EXPECT().FindByIDs(gomock.InAnyOrder([]string{*vm1.ID, *vm2.ID, *vm3.ID})).Return([]*models.VM{vm1, vm2, vm3}, nil) reconciler = &ElfMachineReconciler{ControllerContext: ctrlContext, NewVMService: mockNewVMService} @@ -1348,7 +1349,7 @@ var _ = Describe("ElfMachineReconciler", func() { mockVMService.EXPECT().Get(*vm3.ID).Return(vm3, nil) reconciler = &ElfMachineReconciler{ControllerContext: ctrlContext, NewVMService: mockNewVMService} - hostID, err := reconciler.getVMHostForRollingUpdate(machineContext, placementGroup, hosts) + hostID, err := reconciler.getVMHostForRollingUpdate(machineContext, placementGroup, service.NewHostsFromList(hosts)) Expect(err).To(BeZero()) Expect(hostID).To(Equal("")) Expect(logBuffer.String()).To(ContainSubstring("Host is unavailable: host is in CONNECTED_ERROR status, skip selecting host for VM")) @@ -1360,7 +1361,7 @@ var _ = Describe("ElfMachineReconciler", func() { mockVMService.EXPECT().Get(*vm3.ID).Return(vm3, nil) reconciler = &ElfMachineReconciler{ControllerContext: ctrlContext, NewVMService: mockNewVMService} - hostID, err = reconciler.getVMHostForRollingUpdate(machineContext, placementGroup, hosts) + hostID, err = reconciler.getVMHostForRollingUpdate(machineContext, placementGroup, service.NewHostsFromList(hosts)) Expect(err).To(BeZero()) Expect(hostID).To(Equal("")) Expect(logBuffer.String()).To(ContainSubstring("Host not found, skip selecting host for VM")) @@ -1377,7 +1378,7 @@ var _ = Describe("ElfMachineReconciler", func() { mockVMService.EXPECT().Get(*vm3.ID).Return(vm3, nil) reconciler = &ElfMachineReconciler{ControllerContext: ctrlContext, NewVMService: mockNewVMService} - hostID, err = reconciler.getVMHostForRollingUpdate(machineContext, placementGroup, hosts) + hostID, err = reconciler.getVMHostForRollingUpdate(machineContext, placementGroup, service.NewHostsFromList(hosts)) Expect(err).To(BeZero()) Expect(hostID).To(Equal(*vm3.Host.ID)) Expect(logBuffer.String()).To(ContainSubstring("Selected the host server for VM since the placement group is full")) @@ -1389,7 +1390,7 @@ var _ = Describe("ElfMachineReconciler", func() { mockVMService.EXPECT().Get(*vm3.ID).Return(vm3, nil) reconciler = &ElfMachineReconciler{ControllerContext: ctrlContext, NewVMService: mockNewVMService} - hostID, err = reconciler.getVMHostForRollingUpdate(machineContext, placementGroup, hosts) + hostID, err = reconciler.getVMHostForRollingUpdate(machineContext, placementGroup, service.NewHostsFromList(hosts)) Expect(err).To(BeZero()) Expect(hostID).To(Equal(*vm3.Host.ID)) Expect(logBuffer.String()).To(ContainSubstring("Selected the host server for VM since the placement group is full")) @@ -1428,7 +1429,7 @@ var _ = Describe("ElfMachineReconciler", func() { placementGroupName, err := towerresources.GetVMPlacementGroupName(ctx, ctrlContext.Client, machine, cluster) Expect(err).NotTo(HaveOccurred()) mockVMService.EXPECT().GetVMPlacementGroup(placementGroupName).Return(placementGroup, nil) - mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return([]*models.Host{host1}, nil) + mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return(service.NewHosts(host1), nil) mockVMService.EXPECT().FindByIDs(gomock.InAnyOrder([]string{*vm1.ID})).Return([]*models.VM{vm1}, nil) reconciler := &ElfMachineReconciler{ControllerContext: ctrlContext, NewVMService: mockNewVMService} @@ -1440,7 +1441,7 @@ var _ = Describe("ElfMachineReconciler", func() { elfMachine.Status.Conditions = nil mockVMService.EXPECT().GetVMPlacementGroup(placementGroupName).Return(placementGroup, nil) - mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return([]*models.Host{host1, host2}, nil) + mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return(service.NewHosts(host1, host2), nil) mockVMService.EXPECT().FindByIDs(gomock.InAnyOrder([]string{*vm1.ID})).Return([]*models.VM{vm1}, nil) reconciler = &ElfMachineReconciler{ControllerContext: ctrlContext, NewVMService: mockNewVMService} @@ -1451,7 +1452,7 @@ var _ = Describe("ElfMachineReconciler", func() { placementGroup.Vms = []*models.NestedVM{} mockVMService.EXPECT().GetVMPlacementGroup(placementGroupName).Return(placementGroup, nil) - mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return([]*models.Host{host1}, nil) + mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return(service.NewHosts(host1), nil) mockVMService.EXPECT().FindByIDs(gomock.InAnyOrder([]string{})).Return([]*models.VM{}, nil) reconciler = &ElfMachineReconciler{ControllerContext: ctrlContext, NewVMService: mockNewVMService} @@ -1482,7 +1483,7 @@ var _ = Describe("ElfMachineReconciler", func() { placementGroupName, err := towerresources.GetVMPlacementGroupName(ctx, ctrlContext.Client, machine, cluster) Expect(err).NotTo(HaveOccurred()) mockVMService.EXPECT().GetVMPlacementGroup(placementGroupName).Return(placementGroup, nil) - mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return([]*models.Host{host}, nil) + mockVMService.EXPECT().GetHostsByCluster(elfCluster.Spec.Cluster).Return(service.NewHosts(host), nil) mockVMService.EXPECT().FindByIDs(gomock.InAnyOrder([]string{*vm1.ID})).Return([]*models.VM{vm1}, nil) reconciler := &ElfMachineReconciler{ControllerContext: ctrlContext, NewVMService: mockNewVMService} @@ -1497,9 +1498,7 @@ var _ = Describe("ElfMachineReconciler", func() { }) Context("Get Available Hosts For VM", func() { - It("should return the available hosts when the virtual machine has not been created or is not powered on", func() { - vm := fake.NewTowerVMFromElfMachine(elfMachine) - vm.Status = models.NewVMStatus(models.VMStatusSTOPPED) + It("should return the available hosts", func() { host1 := fake.NewTowerHost() host1.AllocatableMemoryBytes = service.TowerMemory(0) host2 := fake.NewTowerHost() @@ -1508,69 +1507,47 @@ var _ = Describe("ElfMachineReconciler", func() { ctrlContext := newCtrlContexts(elfCluster, cluster, elfMachine, machine, secret, kcp) machineContext := newMachineContext(ctrlContext, elfCluster, cluster, elfMachine, machine, mockVMService) + // virtual machine has not been created + reconciler := &ElfMachineReconciler{ControllerContext: ctrlContext, NewVMService: mockNewVMService} - availableHosts := reconciler.getAvailableHostsForVM(machineContext, nil, sets.Set[string]{}, nil) + availableHosts := reconciler.getAvailableHostsForVM(machineContext, nil, service.NewHosts(), nil) Expect(availableHosts).To(BeEmpty()) - availableHosts = reconciler.getAvailableHostsForVM(machineContext, nil, sets.Set[string]{}.Insert(*host2.ID), nil) + availableHosts = reconciler.getAvailableHostsForVM(machineContext, nil, service.NewHosts(host2), nil) Expect(availableHosts).To(BeEmpty()) - availableHosts = reconciler.getAvailableHostsForVM(machineContext, []*models.Host{host1, host2, host3}, sets.Set[string]{}.Insert(*host3.ID), vm) + availableHosts = reconciler.getAvailableHostsForVM(machineContext, service.NewHosts(host1, host2, host3), service.NewHosts(host3), nil) Expect(availableHosts).To(ContainElements(host2)) - availableHosts = reconciler.getAvailableHostsForVM(machineContext, []*models.Host{host1, host2, host3}, sets.Set[string]{}.Insert(*host1.ID, *host2.ID, *host3.ID), vm) + availableHosts = reconciler.getAvailableHostsForVM(machineContext, service.NewHosts(host1, host2, host3), service.NewHosts(host1, host2, host3), nil) Expect(availableHosts).To(BeEmpty()) - }) - It("should return the available hosts when the virtual machine is running", func() { - vm := fake.NewTowerVMFromElfMachine(elfMachine) - vm.Status = models.NewVMStatus(models.VMStatusRUNNING) - vm.Host = &models.NestedHost{ID: service.TowerString(fake.ID())} - host1 := fake.NewTowerHost() - host1.Status = models.NewHostStatus(models.HostStatusSESSIONEXPIRED) - host2 := fake.NewTowerHost() - host3 := fake.NewTowerHost() + // virtual machine is not powered on - ctrlContext := newCtrlContexts(elfCluster, cluster, elfMachine, machine, secret, kcp) - machineContext := newMachineContext(ctrlContext, elfCluster, cluster, elfMachine, machine, mockVMService) - - vm.Host.ID = host2.ID - reconciler := &ElfMachineReconciler{ControllerContext: ctrlContext, NewVMService: mockNewVMService} - availableHosts := reconciler.getAvailableHostsForVM(machineContext, []*models.Host{host1, host2, host3}, sets.Set[string]{}, vm) - Expect(availableHosts).To(ContainElements(host2, host3)) - - availableHosts = reconciler.getAvailableHostsForVM(machineContext, []*models.Host{host1, host2, host3}, sets.Set[string]{}.Insert(*host1.ID), vm) - Expect(availableHosts).To(ContainElements(host2, host3)) - - availableHosts = reconciler.getAvailableHostsForVM(machineContext, []*models.Host{host1, host2, host3}, sets.Set[string]{}.Insert(*host2.ID), vm) - Expect(availableHosts).To(ContainElements(host3)) - - availableHosts = reconciler.getAvailableHostsForVM(machineContext, []*models.Host{host1, host2, host3}, sets.Set[string]{}.Insert(*host3.ID), vm) - Expect(availableHosts).To(ContainElements(host2)) - - availableHosts = reconciler.getAvailableHostsForVM(machineContext, []*models.Host{host1, host2, host3}, sets.Set[string]{}.Insert(*host1.ID, *host2.ID), vm) - Expect(availableHosts).To(ContainElements(host3)) - - availableHosts = reconciler.getAvailableHostsForVM(machineContext, []*models.Host{host1, host2, host3}, sets.Set[string]{}.Insert(*host1.ID, *host3.ID), vm) - Expect(availableHosts).To(ContainElements(host2)) + vm := fake.NewTowerVMFromElfMachine(elfMachine) + vm.Status = models.NewVMStatus(models.VMStatusSTOPPED) - availableHosts = reconciler.getAvailableHostsForVM(machineContext, []*models.Host{host1, host2, host3}, sets.Set[string]{}.Insert(*host2.ID, *host3.ID), vm) + availableHosts = reconciler.getAvailableHostsForVM(machineContext, nil, service.NewHosts(), vm) Expect(availableHosts).To(BeEmpty()) - availableHosts = reconciler.getAvailableHostsForVM(machineContext, []*models.Host{host1, host2, host3}, sets.Set[string]{}.Insert(*host1.ID, *host2.ID, *host3.ID), vm) + availableHosts = reconciler.getAvailableHostsForVM(machineContext, nil, service.NewHosts(host2), vm) Expect(availableHosts).To(BeEmpty()) - availableHosts = reconciler.getAvailableHostsForVM(machineContext, []*models.Host{host1, host2}, sets.Set[string]{}.Insert(*host2.ID, *host3.ID), vm) - Expect(availableHosts).To(BeEmpty()) + availableHosts = reconciler.getAvailableHostsForVM(machineContext, service.NewHosts(host1, host2, host3), service.NewHosts(host3), vm) + Expect(availableHosts).To(ContainElements(host2)) - availableHosts = reconciler.getAvailableHostsForVM(machineContext, []*models.Host{host2, host3}, sets.Set[string]{}.Insert(*host2.ID, *host3.ID), vm) + availableHosts = reconciler.getAvailableHostsForVM(machineContext, service.NewHosts(host1, host2, host3), service.NewHosts(host1, host2, host3), vm) Expect(availableHosts).To(BeEmpty()) - availableHosts = reconciler.getAvailableHostsForVM(machineContext, nil, sets.Set[string]{}, vm) - Expect(availableHosts).To(BeEmpty()) + // virtual machine is powered on + vm.Status = models.NewVMStatus(models.VMStatusRUNNING) + vm.Host = &models.NestedHost{ID: host1.ID} - availableHosts = reconciler.getAvailableHostsForVM(machineContext, nil, sets.Set[string]{}.Insert(*host3.ID), vm) + availableHosts = reconciler.getAvailableHostsForVM(machineContext, service.NewHosts(host1, host2, host3), service.NewHosts(host1, host2, host3), vm) Expect(availableHosts).To(BeEmpty()) + + availableHosts = reconciler.getAvailableHostsForVM(machineContext, service.NewHosts(host1, host2, host3), service.NewHosts(host2, host3), vm) + Expect(availableHosts).To(ContainElements(host1)) }) }) diff --git a/controllers/vm_limiter.go b/controllers/vm_limiter.go index a8c593d1..e0efcc99 100644 --- a/controllers/vm_limiter.go +++ b/controllers/vm_limiter.go @@ -27,7 +27,6 @@ import ( const ( creationTimeout = time.Minute * 6 vmOperationRateLimit = time.Second * 6 - vmMigrationTimeout = time.Minute * 20 placementGroupSilenceTime = time.Minute * 30 placementGroupCreationLockKey = "%s:creation" ) @@ -35,7 +34,6 @@ const ( var vmStatusMap = make(map[string]time.Time) var limiterLock sync.Mutex var vmOperationMap = make(map[string]time.Time) -var placementGroupVMMigrationMap = make(map[string]time.Time) var vmOperationLock sync.Mutex var placementGroupOperationMap = make(map[string]time.Time) @@ -91,32 +89,6 @@ func acquireTicketForUpdatingVM(vmName string) bool { return true } -// acquireTicketForPlacementGroupVMMigration returns whether virtual machine migration -// of placement group operation can be performed. -func acquireTicketForPlacementGroupVMMigration(groupName string) bool { - vmOperationLock.Lock() - defer vmOperationLock.Unlock() - - if status, ok := placementGroupVMMigrationMap[groupName]; ok { - if !time.Now().After(status.Add(vmMigrationTimeout)) { - return false - } - } - - placementGroupVMMigrationMap[groupName] = time.Now() - - return true -} - -// releaseTicketForPlacementGroupVMMigration releases the virtual machine migration -// of placement group being operated. -func releaseTicketForPlacementGroupVMMigration(groupName string) { - vmOperationLock.Lock() - defer vmOperationLock.Unlock() - - delete(placementGroupVMMigrationMap, groupName) -} - // acquireTicketForPlacementGroupOperation returns whether placement group operation // can be performed. func acquireTicketForPlacementGroupOperation(groupName string) bool { diff --git a/controllers/vm_limiter_test.go b/controllers/vm_limiter_test.go index 81badabd..37b259c5 100644 --- a/controllers/vm_limiter_test.go +++ b/controllers/vm_limiter_test.go @@ -85,27 +85,6 @@ var _ = Describe("VM Operation Limiter", func() { }) }) -var _ = Describe("Placement Group VM Migration Limiter", func() { - var groupName string - - BeforeEach(func() { - groupName = fake.UUID() - }) - - It("acquireTicketForPlacementGroupVMMigration", func() { - Expect(acquireTicketForPlacementGroupVMMigration(groupName)).To(BeTrue()) - Expect(placementGroupVMMigrationMap).To(HaveKey(groupName)) - - Expect(acquireTicketForPlacementGroupVMMigration(groupName)).To(BeFalse()) - releaseTicketForPlacementGroupVMMigration(groupName) - Expect(placementGroupVMMigrationMap).NotTo(HaveKey(groupName)) - - placementGroupVMMigrationMap[groupName] = time.Now().Add(-vmMigrationTimeout) - Expect(acquireTicketForPlacementGroupVMMigration(groupName)).To(BeTrue()) - Expect(placementGroupVMMigrationMap).To(HaveKey(groupName)) - }) -}) - var _ = Describe("Placement Group Operation Limiter", func() { var groupName string diff --git a/pkg/service/collections.go b/pkg/service/collections.go new file mode 100644 index 00000000..05c78d8a --- /dev/null +++ b/pkg/service/collections.go @@ -0,0 +1,147 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package service + +import ( + "fmt" + + "github.com/smartxworks/cloudtower-go-sdk/v2/models" + "k8s.io/apimachinery/pkg/util/sets" +) + +// Hosts is a set of hosts. +type Hosts map[string]*models.Host + +// NewHosts creates a Hosts. from a list of values. +func NewHosts(hosts ...*models.Host) Hosts { + ss := make(Hosts, len(hosts)) + ss.Insert(hosts...) + return ss +} + +// NewHostsFromList creates a Hosts from the given host slice. +func NewHostsFromList(hosts []*models.Host) Hosts { + ss := make(Hosts, len(hosts)) + for i := range hosts { + ss.Insert(hosts[i]) + } + return ss +} + +func (s Hosts) Insert(hosts ...*models.Host) { + for i := range hosts { + if hosts[i] != nil { + h := hosts[i] + s[*h.ID] = h + } + } +} + +func (s Hosts) Contains(hostID string) bool { + _, ok := s[hostID] + return ok +} + +// Len returns the size of the set. +func (s Hosts) Len() int { + return len(s) +} + +func (s Hosts) IsEmpty() bool { + return len(s) == 0 +} + +func (s Hosts) String() string { + str := "" + for _, host := range s { + str += fmt.Sprintf("{id: %s,name: %s},", GetTowerString(host.ID), GetTowerString(host.Name)) + } + + return fmt.Sprintf("[%s]", str) +} + +// Available returns a Hosts with available hosts. +func (s Hosts) Available(memory int64) Hosts { + return s.Filter(func(h *models.Host) bool { + ok, _ := IsAvailableHost(h, memory) + return ok + }) +} + +// Get returns a Host of the specified host. +func (s Hosts) Get(hostID string) *models.Host { + if host, ok := s[hostID]; ok { + return host + } + return nil +} + +// Find returns a Hosts of the specified hosts. +func (s Hosts) Find(targetHosts sets.Set[string]) Hosts { + return s.Filter(func(h *models.Host) bool { + return targetHosts.Has(*h.ID) + }) +} + +// UnsortedList returns the slice with contents in random order. +func (s Hosts) UnsortedList() []*models.Host { + res := make([]*models.Host, 0, len(s)) + for _, value := range s { + res = append(res, value) + } + return res +} + +// Difference returns a copy without hosts that are in the given collection. +func (s Hosts) Difference(hosts Hosts) Hosts { + return s.Filter(func(h *models.Host) bool { + _, found := hosts[*h.ID] + return !found + }) +} + +// newFilteredHostCollection creates a Hosts from a filtered list of values. +func newFilteredHostCollection(filter Func, hosts ...*models.Host) Hosts { + ss := make(Hosts, len(hosts)) + for i := range hosts { + h := hosts[i] + if filter(h) { + ss.Insert(h) + } + } + return ss +} + +// Filter returns a Hosts containing only the Hosts that match all of the given HostFilters. +func (s Hosts) Filter(filters ...Func) Hosts { + return newFilteredHostCollection(And(filters...), s.UnsortedList()...) +} + +// Func is the functon definition for a filter. +type Func func(host *models.Host) bool + +// And returns a filter that returns true if all of the given filters returns true. +func And(filters ...Func) Func { + return func(host *models.Host) bool { + for _, f := range filters { + if !f(host) { + return false + } + } + return true + } +} diff --git a/pkg/service/collections_test.go b/pkg/service/collections_test.go new file mode 100644 index 00000000..cf74e046 --- /dev/null +++ b/pkg/service/collections_test.go @@ -0,0 +1,69 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package service + +import ( + "testing" + + "github.com/onsi/gomega" + "github.com/smartxworks/cloudtower-go-sdk/v2/models" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/utils/pointer" +) + +func TestHostCollection(t *testing.T) { + g := gomega.NewGomegaWithT(t) + + t.Run("Find", func(t *testing.T) { + host1 := &models.Host{ID: TowerString("1"), Name: TowerString("host1")} + host2 := &models.Host{ID: TowerString("2"), Name: TowerString("host2")} + + hosts := NewHosts() + g.Expect(hosts.Find(sets.Set[string]{}.Insert(*host1.ID)).Len()).To(gomega.Equal(0)) + + hosts = NewHostsFromList([]*models.Host{host1, host2}) + g.Expect(hosts.Get(*host1.ID)).To(gomega.Equal(host1)) + g.Expect(hosts.Get(*TowerString("404"))).To(gomega.BeNil()) + g.Expect(hosts.Find(sets.Set[string]{}.Insert(*host1.ID)).Contains(*host1.ID)).To(gomega.BeTrue()) + g.Expect(hosts.Find(sets.Set[string]{}.Insert(*host1.ID)).Len()).To(gomega.Equal(1)) + }) + + t.Run("Available", func(t *testing.T) { + host1 := &models.Host{ID: TowerString("1"), Name: TowerString("host1"), AllocatableMemoryBytes: pointer.Int64(1), Status: models.NewHostStatus(models.HostStatusCONNECTEDHEALTHY)} + host2 := &models.Host{ID: TowerString("2"), Name: TowerString("host2"), AllocatableMemoryBytes: pointer.Int64(2), Status: models.NewHostStatus(models.HostStatusCONNECTEDHEALTHY)} + + hosts := NewHosts() + g.Expect(hosts.Available(0).Len()).To(gomega.Equal(0)) + + hosts = NewHostsFromList([]*models.Host{host1, host2}) + availableHosts := hosts.Available(2) + g.Expect(availableHosts.Len()).To(gomega.Equal(1)) + g.Expect(availableHosts.Contains(*host2.ID)).To(gomega.BeTrue()) + }) + + t.Run("Difference", func(t *testing.T) { + host1 := &models.Host{ID: TowerString("1"), Name: TowerString("host1")} + host2 := &models.Host{ID: TowerString("2"), Name: TowerString("host2")} + + g.Expect(NewHosts().Difference(NewHosts()).Len()).To(gomega.Equal(0)) + g.Expect(NewHosts().Difference(NewHosts(host1)).Len()).To(gomega.Equal(0)) + g.Expect(NewHosts(host1).Difference(NewHosts(host1)).Len()).To(gomega.Equal(0)) + g.Expect(NewHosts(host1).Difference(NewHosts()).Contains(*host1.ID)).To(gomega.BeTrue()) + g.Expect(NewHosts(host1).Difference(NewHosts(host2)).Contains(*host1.ID)).To(gomega.BeTrue()) + g.Expect(NewHosts(host1, host2).Difference(NewHosts(host2)).Contains(*host1.ID)).To(gomega.BeTrue()) + }) +} diff --git a/pkg/service/mock_services/vm_mock.go b/pkg/service/mock_services/vm_mock.go index ffed9efd..968f2293 100644 --- a/pkg/service/mock_services/vm_mock.go +++ b/pkg/service/mock_services/vm_mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/smartxworks/cluster-api-provider-elf/pkg/service/vm.go +// Source: /root/github/cluster-api-provider-elf/pkg/service/vm.go // Package mock_services is a generated GoMock package. package mock_services @@ -11,6 +11,7 @@ import ( gomock "github.com/golang/mock/gomock" models "github.com/smartxworks/cloudtower-go-sdk/v2/models" v1beta1 "github.com/smartxworks/cluster-api-provider-elf/api/v1beta1" + service "github.com/smartxworks/cluster-api-provider-elf/pkg/service" v1beta10 "sigs.k8s.io/cluster-api/api/v1beta1" ) @@ -217,10 +218,10 @@ func (mr *MockVMServiceMockRecorder) GetHost(id interface{}) *gomock.Call { } // GetHostsByCluster mocks base method. -func (m *MockVMService) GetHostsByCluster(clusterID string) ([]*models.Host, error) { +func (m *MockVMService) GetHostsByCluster(clusterID string) (service.Hosts, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetHostsByCluster", clusterID) - ret0, _ := ret[0].([]*models.Host) + ret0, _ := ret[0].(service.Hosts) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -277,18 +278,18 @@ func (mr *MockVMServiceMockRecorder) GetVMPlacementGroup(name interface{}) *gomo } // GetVMTemplate mocks base method. -func (m *MockVMService) GetVMTemplate(id string) (*models.ContentLibraryVMTemplate, error) { +func (m *MockVMService) GetVMTemplate(template string) (*models.ContentLibraryVMTemplate, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetVMTemplate", id) + ret := m.ctrl.Call(m, "GetVMTemplate", template) ret0, _ := ret[0].(*models.ContentLibraryVMTemplate) ret1, _ := ret[1].(error) return ret0, ret1 } // GetVMTemplate indicates an expected call of GetVMTemplate. -func (mr *MockVMServiceMockRecorder) GetVMTemplate(id interface{}) *gomock.Call { +func (mr *MockVMServiceMockRecorder) GetVMTemplate(template interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVMTemplate", reflect.TypeOf((*MockVMService)(nil).GetVMTemplate), id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVMTemplate", reflect.TypeOf((*MockVMService)(nil).GetVMTemplate), template) } // GetVlan mocks base method. diff --git a/pkg/service/util.go b/pkg/service/util.go index 4aa78163..0190deab 100644 --- a/pkg/service/util.go +++ b/pkg/service/util.go @@ -77,84 +77,6 @@ func IsAvailableHost(host *models.Host, memory int64) (bool, string) { return true, "" } -// GetAvailableHosts returns the available hosts. -func GetAvailableHosts(hosts []*models.Host, memory int64) []*models.Host { - var availableHosts []*models.Host - for i := 0; i < len(hosts); i++ { - if ok, _ := IsAvailableHost(hosts[i], memory); ok { - availableHosts = append(availableHosts, hosts[i]) - } - } - - return availableHosts -} - -func GetUnavailableHostInfo(hosts []*models.Host, memory int64) map[string]string { - info := make(map[string]string) - for i := 0; i < len(hosts); i++ { - ok, message := IsAvailableHost(hosts[i], memory) - if !ok { - info[*hosts[i].Name] = message - } - } - - return info -} - -func ContainsUnavailableHost(hosts []*models.Host, hostIDs []string, memory int64) bool { - if len(hosts) == 0 || len(hostIDs) == 0 { - return true - } - - hostMap := make(map[string]*models.Host) - for i := 0; i < len(hosts); i++ { - hostMap[*hosts[i].ID] = hosts[i] - } - - for i := 0; i < len(hostIDs); i++ { - host, ok := hostMap[hostIDs[i]] - if !ok { - return true - } - - if ok, _ := IsAvailableHost(host, memory); !ok { - return true - } - } - - return false -} - -func GetHostFromList(hostID string, hosts []*models.Host) *models.Host { - for i := 0; i < len(hosts); i++ { - if *hosts[i].ID == hostID { - return hosts[i] - } - } - - return nil -} - -func HostsToSet(hosts []*models.Host) sets.Set[string] { - hostSet := sets.Set[string]{} - for i := 0; i < len(hosts); i++ { - hostSet.Insert(*hosts[i].ID) - } - - return hostSet -} - -func FilterHosts(hosts []*models.Host, needFilteredHosts sets.Set[string]) []*models.Host { - var filteredHosts []*models.Host - for i := 0; i < len(hosts); i++ { - if !needFilteredHosts.Has(*hosts[i].ID) { - filteredHosts = append(filteredHosts, hosts[i]) - } - } - - return filteredHosts -} - // GetVMsInPlacementGroup returns a Set of IDs of the virtual machines in the placement group. func GetVMsInPlacementGroup(placementGroup *models.VMPlacementGroup) sets.Set[string] { placementGroupVMSet := sets.Set[string]{} @@ -243,6 +165,14 @@ func GetTowerInt32(ptr *int32) int32 { return *ptr } +func GetTowerInt64(ptr *int64) int64 { + if ptr == nil { + return 0 + } + + return *ptr +} + func GetTowerTaskStatus(ptr *models.TaskStatus) string { if ptr == nil { return "" diff --git a/pkg/service/util_test.go b/pkg/service/util_test.go index d743cc2f..28253a6b 100644 --- a/pkg/service/util_test.go +++ b/pkg/service/util_test.go @@ -100,21 +100,3 @@ func TestIsAvailableHost(t *testing.T) { g.Expect(message).To(gomega.ContainSubstring("3")) }) } - -func TestContainsUnavailableHost(t *testing.T) { - g := gomega.NewGomegaWithT(t) - - t.Run("should return false when has unavailable host", func(t *testing.T) { - hosts := []*models.Host{{ID: pointer.String("1"), AllocatableMemoryBytes: pointer.Int64(1), Status: models.NewHostStatus(models.HostStatusCONNECTEDHEALTHY)}} - - g.Expect(ContainsUnavailableHost(nil, []string{"0"}, 2)).To(gomega.BeTrue()) - - g.Expect(ContainsUnavailableHost(hosts, nil, 2)).To(gomega.BeTrue()) - - g.Expect(ContainsUnavailableHost(hosts, []string{"0"}, 2)).To(gomega.BeTrue()) - - g.Expect(ContainsUnavailableHost(hosts, []string{"1"}, 2)).To(gomega.BeTrue()) - - g.Expect(ContainsUnavailableHost(hosts, []string{"1"}, 1)).To(gomega.BeFalse()) - }) -} diff --git a/pkg/service/vm.go b/pkg/service/vm.go index 2421259f..dffd738e 100644 --- a/pkg/service/vm.go +++ b/pkg/service/vm.go @@ -60,7 +60,7 @@ type VMService interface { WaitTask(id string, timeout, interval time.Duration) (*models.Task, error) GetCluster(id string) (*models.Cluster, error) GetHost(id string) (*models.Host, error) - GetHostsByCluster(clusterID string) ([]*models.Host, error) + GetHostsByCluster(clusterID string) (Hosts, error) GetVlan(id string) (*models.Vlan, error) UpsertLabel(key, value string) (*models.Label, error) DeleteLabel(key, value string, strict bool) (string, error) @@ -488,7 +488,7 @@ func (svr *TowerVMService) GetHost(id string) (*models.Host, error) { return getHostsResp.Payload[0], nil } -func (svr *TowerVMService) GetHostsByCluster(clusterID string) ([]*models.Host, error) { +func (svr *TowerVMService) GetHostsByCluster(clusterID string) (Hosts, error) { getHostsParams := clienthost.NewGetHostsParams() getHostsParams.RequestBody = &models.GetHostsRequestBody{ Where: &models.HostWhereInput{ @@ -507,7 +507,7 @@ func (svr *TowerVMService) GetHostsByCluster(clusterID string) ([]*models.Host, return nil, errors.New(HostNotFound) } - return getHostsResp.Payload, nil + return NewHostsFromList(getHostsResp.Payload), nil } // GetVlan searches for a vlan. diff --git a/pkg/util/kcp/kcp.go b/pkg/util/kcp/kcp.go index b2ef2992..fc850937 100644 --- a/pkg/util/kcp/kcp.go +++ b/pkg/util/kcp/kcp.go @@ -18,8 +18,22 @@ limitations under the License. import ( controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1" + "sigs.k8s.io/cluster-api/util/conditions" ) +// IsKCPRollingUpdate returns whether KCP is in scaling down. +// +// When KCP is in rolling update, KCP controller marks +// MachinesSpecUpToDateCondition to false and RollingUpdateInProgressReason as Reason. +// +// When all machines are up to date, KCP controller marks MachinesSpecUpToDateCondition to true. +// +// For more information about KCP MachinesSpecUpToDateCondition and RollingUpdateInProgressReason, refer to https://github.com/kubernetes-sigs/cluster-api/blob/main/api/v1beta1/condition_consts.go +func IsKCPRollingUpdate(kcp *controlplanev1.KubeadmControlPlane) bool { + return conditions.IsFalse(kcp, controlplanev1.MachinesSpecUpToDateCondition) && + conditions.GetReason(kcp, controlplanev1.MachinesSpecUpToDateCondition) == controlplanev1.RollingUpdateInProgressReason +} + // IsKCPRollingUpdateFirstMachine returns true if KCP is in rolling update and creating the first CP Machine. // // KCP rollout algorithm is as follows: @@ -36,7 +50,7 @@ import ( // For more information about KCP replicas, refer to https://github.com/kubernetes-sigs/cluster-api/blob/main/controlplane/kubeadm/api/v1beta1/kubeadm_control_plane_types.go // For more information about KCP rollout, refer to https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20191017-kubeadm-based-control-plane.md#kubeadmcontrolplane-rollout func IsKCPRollingUpdateFirstMachine(kcp *controlplanev1.KubeadmControlPlane) bool { - return *kcp.Spec.Replicas < kcp.Status.Replicas && kcp.Status.UpdatedReplicas == 1 + return IsKCPRollingUpdate(kcp) && kcp.Status.UpdatedReplicas == 1 } // IsKCPInScalingDown returns whether KCP is in scaling down.