diff --git a/images/virtualization-artifact/pkg/controller/workload-updater/internal/service/one_shot_migration.go b/images/virtualization-artifact/pkg/controller/workload-updater/internal/service/one_shot_migration.go index aaeaeddf3a..2bc52a6f33 100644 --- a/images/virtualization-artifact/pkg/controller/workload-updater/internal/service/one_shot_migration.go +++ b/images/virtualization-artifact/pkg/controller/workload-updater/internal/service/one_shot_migration.go @@ -21,6 +21,7 @@ import ( "fmt" "log/slog" + corev1 "k8s.io/api/core/v1" virtv1 "kubevirt.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -61,6 +62,15 @@ func (s *OneShotMigrationService) OnceMigrate(ctx context.Context, vm *v1alpha2. return false, nil } + cleanupInProgress, err := s.hasInactiveLauncherPodsInProgress(ctx, kvvmi) + if err != nil { + return false, err + } + if cleanupInProgress { + log.Debug("The virtual machine has inactive launcher pods that are not terminated yet. Skipping migration...") + return false, nil + } + workloadUpdateVMOPs, unmanagedVMOPs, err := s.listVMOPMigrate(ctx, vm.GetName(), vm.GetNamespace()) if err != nil { return false, err @@ -113,6 +123,31 @@ func (s *OneShotMigrationService) setAnnoExpectedValueToKVVMI(ctx context.Contex return object.EnsureAnnotation(ctx, s.client, kvvmi, annotationKey, annotationExpectedValue) } +func (s *OneShotMigrationService) hasInactiveLauncherPodsInProgress(ctx context.Context, kvvmi *virtv1.VirtualMachineInstance) (bool, error) { + podList := &corev1.PodList{} + err := s.client.List(ctx, podList, + client.InNamespace(kvvmi.GetNamespace()), + client.MatchingLabels{ + virtv1.AppLabel: "virt-launcher", + virtv1.CreatedByLabel: string(kvvmi.GetUID()), + }, + ) + if err != nil { + return false, fmt.Errorf("failed to list virtual machine pods: %w", err) + } + + for _, pod := range podList.Items { + if _, active := kvvmi.Status.ActivePods[pod.GetUID()]; active { + continue + } + if pod.Status.Phase != corev1.PodSucceeded && pod.Status.Phase != corev1.PodFailed { + return true, nil + } + } + + return false, nil +} + func newVMOP(prefix, namespace, vmName string) *v1alpha2.VirtualMachineOperation { return vmopbuilder.New( vmopbuilder.WithGenerateName(prefix), diff --git a/images/virtualization-artifact/pkg/controller/workload-updater/internal/service/one_shot_migration_test.go b/images/virtualization-artifact/pkg/controller/workload-updater/internal/service/one_shot_migration_test.go index cd461d7d5a..5a548c6091 100644 --- a/images/virtualization-artifact/pkg/controller/workload-updater/internal/service/one_shot_migration_test.go +++ b/images/virtualization-artifact/pkg/controller/workload-updater/internal/service/one_shot_migration_test.go @@ -21,7 +21,9 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" virtv1 "kubevirt.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -45,6 +47,7 @@ var _ = Describe("TestOnceShotMigrationService", func() { ObjectMeta: metav1.ObjectMeta{ Name: vmName, Namespace: vmNamespace, + UID: types.UID("vmi-uid"), Annotations: map[string]string{ "key": "old-value", }, @@ -53,9 +56,51 @@ var _ = Describe("TestOnceShotMigrationService", func() { Kind: "VirtualMachineInstance", APIVersion: virtv1.GroupVersion.String(), }, + Status: virtv1.VirtualMachineInstanceStatus{ + ActivePods: map[types.UID]string{ + types.UID("active-pod-uid"): "node-a", + }, + }, + } + } + + newInactiveLauncherPod := func(phase corev1.PodPhase) *corev1.Pod { + return &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "inactive-launcher-pod", + Namespace: vmNamespace, + UID: types.UID("inactive-pod-uid"), + Labels: map[string]string{ + virtv1.AppLabel: "virt-launcher", + virtv1.CreatedByLabel: "vmi-uid", + }, + }, + Status: corev1.PodStatus{Phase: phase}, } } + It("should skip migration while an inactive launcher pod is not terminated", func() { + prefix := "vmop-prefix-" + + fakeClient, err := testutil.NewFakeClientWithObjects(newVM(), newKVVMI(), newInactiveLauncherPod(corev1.PodRunning)) + Expect(err).ToNot(HaveOccurred()) + + oneShotMigration := NewOneShotMigrationService(fakeClient, prefix) + + vm := &v1alpha2.VirtualMachine{} + err = fakeClient.Get(context.Background(), client.ObjectKey{Namespace: vmNamespace, Name: vmName}, vm) + Expect(err).ToNot(HaveOccurred()) + + migrate, err := oneShotMigration.OnceMigrate(testutil.ContextBackgroundWithNoOpLogger(), vm, "key", "value") + Expect(err).ToNot(HaveOccurred()) + Expect(migrate).To(BeFalse()) + + vmops := v1alpha2.VirtualMachineOperationList{} + err = fakeClient.List(context.Background(), &vmops) + Expect(err).ToNot(HaveOccurred()) + Expect(vmops.Items).To(BeEmpty()) + }) + It("Retry 10 times expect one migration", func() { prefix := "vmop-prefix-"