Repository: openshift/cluster-kube-descheduler-operator
Pull request #495: [e2e] test softtainter
Author: login: tiraboschi
Labels: name: lgtm
description: Indicates that a PR is ready to be merged.
name: approved
description: Indicates a PR has been approved by an approver from all required OWNERS files.
name: tide/merge-method-squash
description: Denotes a PR that should be squashed by tide when it merges.
Created: 2025-04-09T10:48:33Z | Merged: 2025-04-10T11:11:12Z

Cover also softtainter deployment, its cleanup and its ValidatingAdmissionPolicy in e2e tests.


Diff:
diff --git a/bindata/assets/kube-descheduler/softtaintervalidatingadmissionpolicy.yaml b/bindata/assets/kube-descheduler/softtaintervalidatingadmissionpolicy.yaml
index 767650be5..07771b4d4 100644
--- a/bindata/assets/kube-descheduler/softtaintervalidatingadmissionpolicy.yaml
+++ b/bindata/assets/kube-descheduler/softtaintervalidatingadmissionpolicy.yaml
@@ -20,16 +20,18 @@ spec:
   variables:
     - name: "deschedulerTaintPrefix"
       expression: "'descheduler.openshift.io'"
+    - name: "deschedulerTaintEffect"
+      expression: "'PreferNoSchedule'"
     - name: "oldNonDeschedulerTaints"
-      expression: "has(oldObject.spec.taints) ? oldObject.spec.taints.filter(t, !t.key.contains(variables.deschedulerTaintPrefix)) : []"
+      expression: "has(oldObject.spec.taints) ? oldObject.spec.taints.filter(t, !t.key.contains(variables.deschedulerTaintPrefix) || t.effect != variables.deschedulerTaintEffect) : []"
     - name: "oldTaints"
       expression: "has(oldObject.spec.taints) ? oldObject.spec.taints : []"
     - name: "newNonDeschedulerTaints"
-      expression: "has(object.spec.taints) ? object.spec.taints.filter(t, !t.key.contains(variables.deschedulerTaintPrefix)) : []"
+      expression: "has(object.spec.taints) ? object.spec.taints.filter(t, !t.key.contains(variables.deschedulerTaintPrefix) || t.effect != variables.deschedulerTaintEffect) : []"
     - name: "newTaints"
       expression: "has(object.spec.taints) ? object.spec.taints : []"
     - name: "newDeschedulerTaints"
-      expression: "has(object.spec.taints) ? object.spec.taints.filter(t, t.key.contains(variables.deschedulerTaintPrefix)) : []"
+      expression: "has(object.spec.taints) ? object.spec.taints.filter(t, t.key.contains(variables.deschedulerTaintPrefix) && t.effect == variables.deschedulerTaintEffect) : []"
   validations:
     - expression: |
         oldObject.metadata.filter(k, k != "resourceVersion" && k != "generation" && k != "managedFields").all(k, k in object.metadata) &&
diff --git a/pkg/softtainter/softtainter.go b/pkg/softtainter/softtainter.go
index ae8a36e31..94e3c0013 100644
--- a/pkg/softtainter/softtainter.go
+++ b/pkg/softtainter/softtainter.go
@@ -50,7 +50,6 @@ type softTainterArgs struct {
 	useDeviationThresholds bool
 	thresholds             api.ResourceThresholds
 	targetThresholds       api.ResourceThresholds
-	applySoftTaints        bool
 	mode                   desv1.Mode
 }
 
@@ -108,6 +107,39 @@ func (st *softTainter) Reconcile(ctx context.Context, request reconcile.Request)
 	}
 	st.resyncPeriod = time.Duration(*des.Spec.DeschedulingIntervalSeconds) * time.Second
 
+	nodeSelector := labels.Everything()
+	if policy.NodeSelector != nil {
+		sel, err := labels.Parse(*policy.NodeSelector)
+		if err != nil {
+			return reconcile.Result{}, err
+		}
+		nodeSelector = sel
+	}
+
+	nl := corev1.NodeList{}
+	err = st.client.List(ctx, &nl, &client.ListOptions{LabelSelector: nodeSelector})
+	if err != nil {
+		return reconcile.Result{}, err
+	}
+	nodeList := make([]*corev1.Node, len(nl.Items))
+	for i, node := range nl.Items {
+		nodeList[i] = &node
+	}
+
+	enableSoftTainter := false
+	if des.Spec.ProfileCustomizations != nil && des.Spec.ProfileCustomizations.DevEnableSoftTainter {
+		enableSoftTainter = true
+	}
+
+	if !enableSoftTainter {
+		logger.Info("SoftTainter is disabled, cleaning up eventual leftover taints")
+		err = st.cleanAllSoftTaints(ctx, nodeList)
+		if err != nil {
+			return reconcile.Result{}, err
+		}
+		return reconcile.Result{RequeueAfter: st.resyncPeriod}, nil
+	}
+
 	var lnargs *nodeutilization.LowNodeUtilizationArgs
 
 	for _, p := range policy.Profiles {
@@ -124,17 +156,10 @@ func (st *softTainter) Reconcile(ctx context.Context, request reconcile.Request)
 		logger.Error(err, "reconciliation failed")
 		return reconcile.Result{}, err
 	}
-
-	enableSoftTainter := false
-	if des.Spec.ProfileCustomizations != nil && des.Spec.ProfileCustomizations.DevEnableSoftTainter {
-		enableSoftTainter = true
-	}
-
 	st.args = &softTainterArgs{
 		useDeviationThresholds: lnargs.UseDeviationThresholds,
 		thresholds:             lnargs.Thresholds,
 		targetThresholds:       lnargs.TargetThresholds,
-		applySoftTaints:        enableSoftTainter,
 		mode:                   des.Spec.Mode,
 	}
 
@@ -172,37 +197,10 @@ func (st *softTainter) Reconcile(ctx context.Context, request reconcile.Request)
 
 	st.resourceNames = getResourceNames(st.args.thresholds)
 
-	nodeSelector := labels.Everything()
-	if policy.NodeSelector != nil {
-		sel, err := labels.Parse(*policy.NodeSelector)
-		if err != nil {
-			return reconcile.Result{}, err
-		}
-		nodeSelector = sel
-	}
-
-	nl := corev1.NodeList{}
-	err = st.client.List(ctx, &nl, &client.ListOptions{LabelSelector: nodeSelector})
+	err = st.syncSoftTaints(ctx, nodeList)
 	if err != nil {
 		return reconcile.Result{}, err
 	}
-	nodeList := make([]*corev1.Node, len(nl.Items))
-	for i, node := range nl.Items {
-		nodeList[i] = &node
-	}
-
-	if st.args.applySoftTaints {
-		err = st.syncSoftTaints(ctx, nodeList)
-		if err != nil {
-			return reconcile.Result{}, err
-		}
-	} else {
-		logger.Info("SoftTainter is disabled, cleaning up eventual leftover taints")
-		err = st.cleanAllSoftTaints(ctx, nodeList)
-		if err != nil {
-			return reconcile.Result{}, err
-		}
-	}
 	return reconcile.Result{RequeueAfter: st.resyncPeriod}, nil
 }
 
diff --git a/test/e2e/bindata/assets/00_kube-descheduler-operator-crd.yaml b/test/e2e/bindata/assets/00_kube-descheduler-operator-crd.yaml
index 02dff4752..defbf6f51 100644
--- a/test/e2e/bindata/assets/00_kube-descheduler-operator-crd.yaml
+++ b/test/e2e/bindata/assets/00_kube-descheduler-operator-crd.yaml
@@ -117,6 +117,9 @@ spec:
                     - Low
                     - Medium
                     - High
+                    - AsymmetricLow
+                    - AsymmetricMedium
+                    - AsymmetricHigh
                     - ""
                     type: string
                   devEnableEvictionsInBackground:
@@ -125,6 +128,12 @@ spec:
                       The EvictionsInBackground alpha feature is a subject to change.
                       Currently provided as an experimental feature.
                     type: boolean
+                  devEnableSoftTainter:
+                    description: |-
+                      DevEnableSoftTainter enables SoftTainter alpha feature.
+                      The EnableSoftTainter alpha feature is a subject to change.
+                      Currently provided as an experimental feature.
+                    type: boolean
                   devHighNodeUtilizationThresholds:
                     description: |-
                       devHighNodeUtilizationThresholds enumerates thresholds for node utilization levels.
@@ -137,7 +146,7 @@ spec:
                     - ""
                     type: string
                   devLowNodeUtilizationThresholds:
-                    description: LowNodeUtilizationThresholds enumerates predefined
+                    description: DevLowNodeUtilizationThresholds enumerates predefined
                       experimental thresholds
                     enum:
                     - Low
diff --git a/test/e2e/bindata/assets/07_descheduler-operator.cr.devKubeVirtRelieveAndMigrate.yaml b/test/e2e/bindata/assets/07_descheduler-operator.cr.devKubeVirtRelieveAndMigrate.yaml
new file mode 100644
index 000000000..f5f3e9618
--- /dev/null
+++ b/test/e2e/bindata/assets/07_descheduler-operator.cr.devKubeVirtRelieveAndMigrate.yaml
@@ -0,0 +1,17 @@
+apiVersion: operator.openshift.io/v1
+kind: KubeDescheduler
+metadata:
+  name: cluster
+  namespace: openshift-kube-descheduler-operator
+spec:
+  managementState: Managed
+  deschedulingIntervalSeconds: 30
+  mode: "Automatic"
+  profiles:
+    - DevKubeVirtRelieveAndMigrate
+  profileCustomizations:
+    podLifetime: 10s
+    devEnableSoftTainter: true
+    namespaces:
+      included:
+      - e2e-testdescheduling
diff --git a/test/e2e/operator_test.go b/test/e2e/operator_test.go
index 6b472a7a5..3a461c37a 100644
--- a/test/e2e/operator_test.go
+++ b/test/e2e/operator_test.go
@@ -2,6 +2,7 @@ package e2e
 
 import (
 	"context"
+	"fmt"
 	"os"
 	"sort"
 	"strings"
@@ -11,17 +12,22 @@ import (
 	descv1 "github.com/openshift/cluster-kube-descheduler-operator/pkg/apis/descheduler/v1"
 	deschclient "github.com/openshift/cluster-kube-descheduler-operator/pkg/generated/clientset/versioned"
 	ssscheme "github.com/openshift/cluster-kube-descheduler-operator/pkg/generated/clientset/versioned/scheme"
+	"github.com/openshift/cluster-kube-descheduler-operator/pkg/softtainter"
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
 	v1 "k8s.io/api/core/v1"
 	apiextclientv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/util/wait"
 	k8sclient "k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/rest"
 	"k8s.io/client-go/tools/clientcmd"
 	"k8s.io/klog/v2"
+	"k8s.io/kubernetes/pkg/util/taints"
 	"k8s.io/utils/clock"
 	utilpointer "k8s.io/utils/pointer"
 
@@ -32,6 +38,27 @@ import (
 	"github.com/openshift/library-go/pkg/operator/resource/resourceread"
 )
 
+const (
+	baseConf                                           = "base"
+	kubeVirtRelieveAndMigrateConf                      = "devKubeVirtRelieveAndMigrate"
+	kubeVirtLabelKey                                   = "kubevirt.io/schedulable"
+	kubeVirtLabelValue                                 = "true"
+	workersLabelSelector                               = "node-role.kubernetes.io/worker="
+	softTainterDeploymentName                          = "softtainter"
+	softTainterServiceAccountName                      = "openshift-descheduler-softtainter"
+	softTainterClusterRoleName                         = "openshift-descheduler-softtainter"
+	softTainterClusterRoleBindingName                  = "openshift-descheduler-softtainter"
+	softTainterClusterMonitoringViewClusterRoleBinding = "openshift-descheduler-softtainter-monitoring"
+	softTainterValidatingAdmissionPolicyName           = "openshift-descheduler-softtainter-vap"
+	softTainterValidatingAdmissionPolicyBindingName    = "openshift-descheduler-softtainter-vap-binding"
+)
+
+var operatorConfigs = map[string]string{
+	baseConf:                      "assets/07_descheduler-operator.cr.yaml",
+	kubeVirtRelieveAndMigrateConf: "assets/07_descheduler-operator.cr.devKubeVirtRelieveAndMigrate.yaml",
+}
+var operatorConfigsAppliers = map[string]func() error{}
+
 func TestMain(m *testing.M) {
 	if os.Getenv("KUBECONFIG") == "" {
 		klog.Errorf("KUBECONFIG environment variable not set")
@@ -116,7 +143,7 @@ func TestMain(m *testing.M) {
 					if env.Name == "RELATED_IMAGE_OPERAND_IMAGE" {
 						required.Spec.Template.Spec.Containers[0].Env[i].Value = "quay.io/jchaloup/descheduler:v5.1.1-8"
 					} else if env.Name == "RELATED_IMAGE_SOFTTAINTER_IMAGE" {
-						required.Spec.Template.Spec.Containers[0].Env[i].Value = operator_image
+						required.Spec.Template.Spec.Containers[0].Env[i].Value = registry + "/" + os.Getenv("NAMESPACE") + "/" + operator_image
 					}
 				}
 				_, _, err := resourceapply.ApplyDeployment(
@@ -136,20 +163,42 @@ func TestMain(m *testing.M) {
 				return err
 			},
 		},
-		{
-			path: "assets/07_descheduler-operator.cr.yaml",
-			readerAndApply: func(objBytes []byte) error {
-				requiredObj, err := runtime.Decode(ssscheme.Codecs.UniversalDecoder(descv1.SchemeGroupVersion), objBytes)
+	}
+
+	for k, path := range operatorConfigs {
+		operatorConfigsAppliers[k] = func() error {
+			requiredObj, err := runtime.Decode(ssscheme.Codecs.UniversalDecoder(descv1.SchemeGroupVersion), bindata.MustAsset(path))
+			if err != nil {
+				klog.Errorf("Unable to decode %v: %v", path, err)
+				return err
+			}
+			requiredDesch := requiredObj.(*descv1.KubeDescheduler)
+			existingDesch, err := deschClient.KubedeschedulersV1().KubeDeschedulers(requiredDesch.Namespace).Get(ctx, requiredDesch.Name, metav1.GetOptions{})
+			if err != nil {
+				if apierrors.IsNotFound(err) {
+					_, err = deschClient.KubedeschedulersV1().KubeDeschedulers(requiredDesch.Namespace).Create(ctx, requiredDesch, metav1.CreateOptions{})
+					return err
+				} else {
+					return err
+				}
+			}
+			requiredDesch.Spec.DeepCopyInto(&existingDesch.Spec)
+			existingDesch.ObjectMeta.Annotations = requiredDesch.ObjectMeta.Annotations
+			existingDesch.ObjectMeta.Labels = requiredDesch.ObjectMeta.Labels
+			_, err = deschClient.KubedeschedulersV1().KubeDeschedulers(requiredDesch.Namespace).Update(ctx, existingDesch, metav1.UpdateOptions{})
+			// retry once on conflicts
+			if apierrors.IsConflict(err) {
+				existingDesch, err = deschClient.KubedeschedulersV1().KubeDeschedulers(requiredDesch.Namespace).Get(ctx, requiredDesch.Name, metav1.GetOptions{})
 				if err != nil {
-					klog.Errorf("Unable to decode assets/07_descheduler-operator.cr.yaml: %v", err)
 					return err
 				}
-				requiredDesch := requiredObj.(*descv1.KubeDescheduler)
-
-				_, err = deschClient.KubedeschedulersV1().KubeDeschedulers(requiredDesch.Namespace).Create(ctx, requiredDesch, metav1.CreateOptions{})
-				return err
-			},
-		},
+				requiredDesch.Spec.DeepCopyInto(&existingDesch.Spec)
+				existingDesch.ObjectMeta.Annotations = requiredDesch.ObjectMeta.Annotations
+				existingDesch.ObjectMeta.Labels = requiredDesch.ObjectMeta.Labels
+				_, err = deschClient.KubedeschedulersV1().KubeDeschedulers(requiredDesch.Namespace).Update(ctx, existingDesch, metav1.UpdateOptions{})
+			}
+			return err
+		}
 	}
 
 	// create required resources, e.g. namespace, crd, roles
@@ -168,62 +217,227 @@ func TestMain(m *testing.M) {
 		os.Exit(1)
 	}
 
-	var deschOpPod *corev1.Pod
-	// Wait until the descheduler operator pod is running
-	if err := wait.PollImmediate(5*time.Second, 1*time.Minute, func() (bool, error) {
-		klog.Infof("Listing pods...")
-		podItems, err := kubeClient.CoreV1().Pods(operatorclient.OperatorNamespace).List(ctx, metav1.ListOptions{})
-		if err != nil {
-			klog.Errorf("Unable to list pods: %v", err)
-			return false, nil
-		}
-		for _, pod := range podItems.Items {
-			// skip if pod.Name doesn't have operatorclient.OperandName + '-operator'
-			if !strings.HasPrefix(pod.Name, operatorclient.OperandName+"-operator") {
-				continue
-			}
-			klog.Infof("Checking pod: %v, phase: %v, deletionTS: %v\n", pod.Name, pod.Status.Phase, pod.GetDeletionTimestamp())
-			if pod.Status.Phase == corev1.PodRunning && pod.GetDeletionTimestamp() == nil {
-				deschOpPod = pod.DeepCopy()
-				return true, nil
-			}
-		}
-		return false, nil
-	}); err != nil {
+	// apply base CR for the operator
+	err := operatorConfigsAppliers[baseConf]()
+	if err != nil {
+		klog.Errorf("Unable to apply a CR for Descheduler operator: %v", err)
+		os.Exit(1)
+	}
+
+	// wait for descheduler operator pod to be running
+	deschOpPod, err := waitForPodRunningByNamePrefix(ctx, kubeClient, operatorclient.OperatorNamespace, operatorclient.OperandName+"-operator", "")
+	if err != nil {
 		klog.Errorf("Unable to wait for the Descheduler operator pod to run")
 		os.Exit(1)
 	}
 	klog.Infof("Descheduler operator pod running in %v", deschOpPod.Name)
 
-	var deschPod *corev1.Pod
-	// Wait until the descheduler pod is running
-	if err := wait.PollImmediate(5*time.Second, 2*time.Minute, func() (bool, error) {
-		klog.Infof("Listing pods...")
-		podItems, err := kubeClient.CoreV1().Pods(operatorclient.OperatorNamespace).List(ctx, metav1.ListOptions{})
+	// wait for descheduler pod to be running
+	deschPod, err := waitForPodRunningByNamePrefix(ctx, kubeClient, operatorclient.OperatorNamespace, operatorclient.OperandName, operatorclient.OperandName+"-operator")
+	if err != nil {
+		klog.Errorf("Unable to wait for the Descheduler pod to run")
+		os.Exit(1)
+	}
+	klog.Infof("Descheduler (operand) pod running in %v", deschPod.Name)
+
+	os.Exit(m.Run())
+}
+
+func TestSoftTainterDeployment(t *testing.T) {
+	kubeClient := getKubeClientOrDie()
+	ctx, cancelFnc := context.WithCancel(context.TODO())
+	defer cancelFnc()
+
+	// ensure that softtainter additional objects are not there
+	if err := checkSoftTainterObjects(ctx, kubeClient, operatorclient.OperatorNamespace, false); err != nil {
+		t.Fatalf("Unexpected softTainter object: %v", err)
+	}
+	klog.Infof("No one of the softtainter additonal objects is there")
+
+	// label all the nodes to mock a KubeVirt deployment
+	if err := applyKubeVirtNodeLabel(ctx, kubeClient); err != nil {
+		t.Fatalf("Failed applying KubeVirt node label: %v", err)
+	}
+	defer func(ctx context.Context, kubeClient *k8sclient.Clientset) {
+		err := dropKubeVirtNodeLabel(ctx, kubeClient)
 		if err != nil {
-			klog.Errorf("Unable to list pods: %v", err)
-			return false, nil
+			t.Fatalf("Failed reverting KubeVirt node label: %v", err)
 		}
-		for _, pod := range podItems.Items {
-			// skip if pod.Name _doesn't_ have operatorclient.OperandName (operand should have this)
-			// or if it _has_ operatorclient.OperandName + '-operator'
-			if !strings.HasPrefix(pod.Name, operatorclient.OperandName) || strings.HasPrefix(pod.Name, operatorclient.OperandName+"-operator") {
-				continue
-			}
-			klog.Infof("Checking pod: %v, phase: %v, deletionTS: %v\n", pod.Name, pod.Status.Phase, pod.GetDeletionTimestamp())
-			if pod.Status.Phase == corev1.PodRunning && pod.GetDeletionTimestamp() == nil {
-				deschPod = pod.DeepCopy()
-				return true, nil
-			}
+	}(ctx, kubeClient)
+
+	// apply devKubeVirtRelieveAndMigrate CR for the operator
+	if err := operatorConfigsAppliers[kubeVirtRelieveAndMigrateConf](); err != nil {
+		t.Fatalf("Unable to apply a CR for Descheduler operator: %v", err)
+	}
+	klog.Infof("Descheduler operator is now configured with devKubeVirtRelieveAndMigrate profile")
+	defer func() {
+		if err := operatorConfigsAppliers[baseConf](); err != nil {
+			t.Fatalf("Failed restoring base profile: %v", err)
 		}
-		return false, nil
-	}); err != nil {
-		klog.Errorf("Unable to wait for the Descheduler (operand) pod to run")
-		os.Exit(1)
+	}()
+
+	// wait for softtainter pod to be running
+	stPod, err := waitForPodRunningByNamePrefix(ctx, kubeClient, operatorclient.OperatorNamespace, operatorclient.SoftTainterOperandName, "")
+	if err != nil {
+		t.Fatalf("Unable to wait for the softtainter pod to run")
 	}
+	klog.Infof("SoftTainter pod running in %v", stPod.Name)
+
+	// wait for descheduler pod to be running
+	deschPod, err := waitForPodRunningByNamePrefix(ctx, kubeClient, operatorclient.OperatorNamespace, operatorclient.OperandName, operatorclient.OperandName+"-operator")
+	if err != nil {
+		t.Fatalf("Unable to wait for the Descheduler pod to run")
+	}
+	klog.Infof("Descheduler pod running in %v", deschPod.Name)
+
+	// ensure that all the softtainter additional objects are there
+	if err = checkSoftTainterObjects(ctx, kubeClient, operatorclient.OperatorNamespace, true); err != nil {
+		t.Fatalf("Missing expected softTainter object: %v", err)
+	}
+	klog.Infof("All the softtainter additonal objects got properly created")
+
+	// apply test soft taints
+	if err = applySoftTaints(ctx, kubeClient); err != nil {
+		t.Fatalf("Failed applying softtaints: %v", err)
+	}
+	defer func(ctx context.Context, kubeClient *k8sclient.Clientset) {
+		err := removeSoftTaints(ctx, kubeClient)
+		if err != nil {
+			t.Fatalf("Failed cleaning softtaints: %v", err)
+		}
+	}(ctx, kubeClient)
+
+	// revert to base confing for the operator
+	err = operatorConfigsAppliers["base"]()
+	if err != nil {
+		t.Fatalf("Unable to apply a CR for Descheduler operator: %v", err)
+	}
+	klog.Infof("Descheduler operator is now configured with base profile")
+
+	// wait for softtainter pod to disappear
+	err = waitForPodGoneByNamePrefix(ctx, kubeClient, operatorclient.OperatorNamespace, operatorclient.SoftTainterOperandName, operatorclient.OperandName+"-operator")
+	if err != nil {
+		t.Fatalf("Unable to wait for the softtainter pod to disappear")
+	}
+	klog.Infof("softtainer pod disappeared")
+
+	// ensure that all the softtainter additional objects are gone
+	if err = checkSoftTainterObjects(ctx, kubeClient, operatorclient.OperatorNamespace, false); err != nil {
+		t.Fatalf("Unexpected softTainter object: %v", err)
+	}
+	klog.Infof("No one of the softtainter additonal objects is there")
+
+	// ensure that all the test soft taints got cleaned up
+	softTaint := v1.Taint{Key: softtainter.AppropriatelyUtilizedSoftTaintKey, Value: softtainter.AppropriatelyUtilizedSoftTaintValue, Effect: v1.TaintEffectPreferNoSchedule}
+	nodes, err := kubeClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{LabelSelector: workersLabelSelector})
+	if err != nil {
+		t.Fatalf("Unexpected error fetching nodes: %v", err)
+	}
+	for _, node := range nodes.Items {
+		if taints.TaintExists(node.Spec.Taints, &softTaint) {
+			t.Fatalf("Unexpected leftover softtaint on node %v", node.Name)
+		}
+	}
+	klog.Infof("All the test softtaints got properly cleaned up")
+
+}
+
+func TestSoftTainterVAP(t *testing.T) {
+	kubeClient := getKubeClientOrDie()
+	ctx, cancelFnc := context.WithCancel(context.TODO())
+	defer cancelFnc()
+
+	// label all the nodes to mock a KubeVirt deployment
+	if err := applyKubeVirtNodeLabel(ctx, kubeClient); err != nil {
+		t.Fatalf("Failed applying KubeVirt node label: %v", err)
+	}
+	defer func(ctx context.Context, kubeClient *k8sclient.Clientset) {
+		err := dropKubeVirtNodeLabel(ctx, kubeClient)
+		if err != nil {
+			t.Fatalf("Failed reverting KubeVirt node label: %v", err)
+		}
+	}(ctx, kubeClient)
+
+	// apply devKubeVirtRelieveAndMigrate CR for the operator
+	if err := operatorConfigsAppliers[kubeVirtRelieveAndMigrateConf](); err != nil {
+		t.Fatalf("Unable to apply a CR for Descheduler operator: %v", err)
+	}
+	klog.Infof("Descheduler operator is now configured with devKubeVirtRelieveAndMigrate profile")
+	defer func() {
+		if err := operatorConfigsAppliers[baseConf](); err != nil {
+			t.Fatalf("Failed restoring base profile: %v", err)
+		}
+	}()
+
+	// wait for softtainter pod to be running
+	stPod, err := waitForPodRunningByNamePrefix(ctx, kubeClient, operatorclient.OperatorNamespace, operatorclient.SoftTainterOperandName, "")
+	if err != nil {
+		t.Fatalf("Unable to wait for the softtainter pod to run")
+	}
+	klog.Infof("SoftTainter pod running in %v", stPod.Name)
+
+	saKubeconfig := os.Getenv("KUBECONFIG")
+	saConfig, err := clientcmd.BuildConfigFromFlags("", saKubeconfig)
+	if err != nil {
+		t.Fatalf("Unable to build config: %v", err)
+	}
+	saConfig.Impersonate = rest.ImpersonationConfig{
+		UserName: fmt.Sprintf("system:serviceaccount:%v:%v", operatorclient.OperatorNamespace, softTainterServiceAccountName),
+	}
+
+	stClientset, err := k8sclient.NewForConfig(saConfig)
+	if err != nil {
+		t.Fatalf("Unable to build client: %v", err)
+	}
+
+	nodes, err := stClientset.CoreV1().Nodes().List(ctx, metav1.ListOptions{LabelSelector: workersLabelSelector})
+	if err != nil {
+		t.Fatalf("Unable to fetch nodes: %v", err)
+	}
+	if len(nodes.Items) < 1 {
+		t.Fatalf("Unable to find a test node: %v", err)
+	}
+	tNode := &nodes.Items[0]
+
+	defer func(ctx context.Context, kubeClient *k8sclient.Clientset) {
+		err := removeSoftTaints(ctx, kubeClient)
+		if err != nil {
+			t.Fatalf("Failed cleaning softtaints: %v", err)
+		}
+	}(ctx, kubeClient)
+
+	softTaint := v1.Taint{Key: softtainter.AppropriatelyUtilizedSoftTaintKey, Value: softtainter.AppropriatelyUtilizedSoftTaintValue, Effect: v1.TaintEffectPreferNoSchedule}
+	tryAddingTaintWithExpectedSuccess(ctx, t, stClientset, tNode, &softTaint)
+	klog.Infof("softtainter SA is allowed to apply a softtaint with the right key prefix")
+
+	tryRemovingTaintWithExpectedSuccess(ctx, t, stClientset, tNode, &softTaint)
+	klog.Infof("softtainter SA is allowed to delete a softtaint with the right key prefix")
+
+	badSoftTaint := v1.Taint{Key: "wrongKey", Value: softtainter.AppropriatelyUtilizedSoftTaintValue, Effect: v1.TaintEffectPreferNoSchedule}
+	tryAddingTaintWithExpectedFailure(ctx, t, stClientset, tNode, &badSoftTaint)
+	klog.Infof("softtainter SA is not allowed to apply a softtaint with a wrong key prefix")
+
+	hardTaint := v1.Taint{Key: softtainter.AppropriatelyUtilizedSoftTaintKey, Value: softtainter.AppropriatelyUtilizedSoftTaintValue, Effect: v1.TaintEffectNoSchedule}
+	tryAddingTaintWithExpectedFailure(ctx, t, stClientset, tNode, &hardTaint)
+	klog.Infof("softtainter SA is not allowed to apply hard taints")
+
+	// apply wrong softtaint and hard taint as test executor
+	unremovableTaints := []*v1.Taint{&badSoftTaint, &hardTaint}
+	for _, taint := range unremovableTaints {
+		tryAddingTaintWithExpectedSuccess(ctx, t, kubeClient, tNode, taint)
+	}
+	defer func(ctx context.Context, kubeClient *k8sclient.Clientset, node *v1.Node, taints []*v1.Taint) {
+		for _, taint := range taints {
+			tryRemovingTaintWithExpectedSuccess(ctx, t, kubeClient, tNode, taint)
+		}
+	}(ctx, kubeClient, tNode, unremovableTaints)
+
+	tryRemovingTaintWithExpectedFailure(ctx, t, stClientset, tNode, &badSoftTaint)
+	klog.Infof("softtainter SA is not allowed to remove a softtaint with a wrong key prefix")
+
+	tryRemovingTaintWithExpectedFailure(ctx, t, stClientset, tNode, &hardTaint)
+	klog.Infof("softtainter SA is not allowed to remove a hard taint")
 
-	klog.Infof("Descheduler (operand) pod running in %v", deschPod.Name)
-	os.Exit(m.Run())
 }
 
 func TestDescheduling(t *testing.T) {
@@ -402,3 +616,273 @@ func waitForPodsRunning(ctx context.Context, t *testing.T, clientSet *k8sclient.
 		t.Fatalf("Error waiting for pods running: %v", err)
 	}
 }
+
+func waitForPodRunningByNamePrefix(ctx context.Context, kubeClient *k8sclient.Clientset, namespace, nameprefix, excludedprefix string) (*v1.Pod, error) {
+	var expectedPod *corev1.Pod
+	// Wait until the expected pod is running
+	if err := wait.PollImmediate(5*time.Second, 1*time.Minute, func() (bool, error) {
+		klog.Infof("Listing pods...")
+		podItems, err := kubeClient.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{})
+		if err != nil {
+			klog.Errorf("Unable to list pods: %v", err)
+			return false, nil
+		}
+		for _, pod := range podItems.Items {
+			if !strings.HasPrefix(pod.Name, nameprefix) || (excludedprefix != "" && strings.HasPrefix(pod.Name, excludedprefix)) {
+				continue
+			}
+			klog.Infof("Checking pod: %v, phase: %v, deletionTS: %v\n", pod.Name, pod.Status.Phase, pod.GetDeletionTimestamp())
+			if pod.Status.Phase == corev1.PodRunning && pod.GetDeletionTimestamp() == nil {
+				expectedPod = pod.DeepCopy()
+				return true, nil
+			}
+		}
+		return false, nil
+	}); err != nil {
+		return nil, err
+	}
+	return expectedPod, nil
+}
+
+func waitForPodGoneByNamePrefix(ctx context.Context, kubeClient *k8sclient.Clientset, namespace, nameprefix, excludedprefix string) error {
+	// Wait until a no pods match nameprefix
+	if err := wait.PollImmediate(5*time.Second, 1*time.Minute, func() (bool, error) {
+		klog.Infof("Listing pods...")
+		podItems, err := kubeClient.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{})
+		if err != nil {
+			klog.Errorf("Unable to list pods: %v", err)
+			return false, nil
+		}
+		for _, pod := range podItems.Items {
+			if !strings.HasPrefix(pod.Name, nameprefix) || (excludedprefix != "" && strings.HasPrefix(pod.Name, excludedprefix)) {
+				continue
+			}
+			klog.Infof("Found pod: %v, phase: %v, deletionTS: %v\n", pod.Name, pod.Status.Phase, pod.GetDeletionTimestamp())
+			return false, nil
+		}
+		return true, nil
+	}); err != nil {
+		return err
+	}
+	return nil
+}
+
+func checkSoftTainterObjects(ctx context.Context, kubeClient *k8sclient.Clientset, namespace string, expected bool) error {
+	var obj runtime.Object
+	var err error
+
+	obj, err = kubeClient.AppsV1().Deployments(namespace).Get(ctx, softTainterDeploymentName, metav1.GetOptions{})
+	if cerr := checkExpected(expected, obj, err); cerr != nil {
+		return cerr
+	}
+	obj, err = kubeClient.CoreV1().ServiceAccounts(namespace).Get(ctx, softTainterServiceAccountName, metav1.GetOptions{})
+	if cerr := checkExpected(expected, obj, err); cerr != nil {
+		return cerr
+	}
+	obj, err = kubeClient.RbacV1().ClusterRoles().Get(ctx, softTainterClusterRoleName, metav1.GetOptions{})
+	if cerr := checkExpected(expected, obj, err); cerr != nil {
+		return cerr
+	}
+	obj, err = kubeClient.RbacV1().ClusterRoleBindings().Get(ctx, softTainterClusterRoleBindingName, metav1.GetOptions{})
+	if cerr := checkExpected(expected, obj, err); cerr != nil {
+		return cerr
+	}
+	obj, err = kubeClient.RbacV1().ClusterRoleBindings().Get(ctx, softTainterClusterMonitoringViewClusterRoleBinding, metav1.GetOptions{})
+	if cerr := checkExpected(expected, obj, err); cerr != nil {
+		return cerr
+	}
+	obj, err = kubeClient.AdmissionregistrationV1().ValidatingAdmissionPolicies().Get(ctx, softTainterValidatingAdmissionPolicyName, metav1.GetOptions{})
+	if cerr := checkExpected(expected, obj, err); cerr != nil {
+		return cerr
+	}
+	obj, err = kubeClient.AdmissionregistrationV1().ValidatingAdmissionPolicyBindings().Get(ctx, softTainterValidatingAdmissionPolicyBindingName, metav1.GetOptions{})
+	if cerr := checkExpected(expected, obj, err); cerr != nil {
+		return cerr
+	}
+
+	return nil
+
+}
+
+func checkExpected(expected bool, obj runtime.Object, err error) error {
+	if err != nil {
+		if expected {
+			return err
+		} else {
+			if apierrors.IsNotFound(err) {
+				return nil
+			} else {
+				return err
+			}
+		}
+	} else {
+		if expected {
+			return nil
+		} else {
+			metaObj, merr := meta.Accessor(obj)
+			if merr != nil {
+				return fmt.Errorf("cannot get metadata: %w", merr)
+			}
+			return fmt.Errorf("Found unxepected %v %v", obj.GetObjectKind().GroupVersionKind().String(), metaObj.GetName())
+		}
+	}
+}
+
+func applyKubeVirtNodeLabel(ctx context.Context, kubeClient *k8sclient.Clientset) error {
+	nodes, err := kubeClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{LabelSelector: workersLabelSelector})
+	if err != nil {
+		return err
+	}
+	for _, node := range nodes.Items {
+		if node.Labels[kubeVirtLabelKey] != kubeVirtLabelValue {
+			node.Labels[kubeVirtLabelKey] = kubeVirtLabelValue
+			uerr := updateNodeAndRetryOnConflicts(ctx, kubeClient, &node, metav1.UpdateOptions{})
+			if uerr != nil {
+				return uerr
+			}
+		}
+	}
+	return nil
+}
+
+func dropKubeVirtNodeLabel(ctx context.Context, kubeClient *k8sclient.Clientset) error {
+	nodes, err := kubeClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{LabelSelector: workersLabelSelector})
+	if err != nil {
+		return err
+	}
+	for _, node := range nodes.Items {
+		if node.Labels[kubeVirtLabelKey] == kubeVirtLabelValue {
+			delete(node.Labels, kubeVirtLabelKey)
+			uerr := updateNodeAndRetryOnConflicts(ctx, kubeClient, &node, metav1.UpdateOptions{})
+			if uerr != nil {
+				return uerr
+			}
+		}
+	}
+	return nil
+}
+
+func applySoftTaints(ctx context.Context, kubeClient *k8sclient.Clientset) error {
+	softTaint := v1.Taint{Key: softtainter.AppropriatelyUtilizedSoftTaintKey, Value: softtainter.AppropriatelyUtilizedSoftTaintValue, Effect: v1.TaintEffectPreferNoSchedule}
+	nodes, err := kubeClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{LabelSelector: workersLabelSelector})
+	if err != nil {
+		return err
+	}
+	for _, node := range nodes.Items {
+		tNode, updated, terr := taints.AddOrUpdateTaint(&node, &softTaint)
+		if terr != nil {
+			return terr
+		}
+		if updated {
+			uerr := updateNodeAndRetryOnConflicts(ctx, kubeClient, tNode, metav1.UpdateOptions{})
+			if uerr != nil {
+				return uerr
+			}
+		}
+	}
+	return nil
+}
+
+func removeSoftTaints(ctx context.Context, kubeClient *k8sclient.Clientset) error {
+	softTaint := v1.Taint{Key: softtainter.AppropriatelyUtilizedSoftTaintKey, Value: softtainter.AppropriatelyUtilizedSoftTaintValue, Effect: v1.TaintEffectPreferNoSchedule}
+	nodes, err := kubeClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{LabelSelector: workersLabelSelector})
+	if err != nil {
+		return err
+	}
+	for _, node := range nodes.Items {
+		tNode, updated, terr := taints.RemoveTaint(&node, &softTaint)
+		if terr != nil {
+			return terr
+		}
+		if updated {
+			uerr := updateNodeAndRetryOnConflicts(ctx, kubeClient, tNode, metav1.UpdateOptions{})
+			if uerr != nil {
+				return uerr
+			}
+		}
+	}
+	return nil
+}
+
+func updateNodeAndRetryOnConflicts(ctx context.Context, kubeClient *k8sclient.Clientset, node *corev1.Node, opts metav1.UpdateOptions) error {
+	uNode, uerr := kubeClient.CoreV1().Nodes().Update(ctx, node, opts)
+	if uerr != nil {
+		if apierrors.IsConflict(uerr) {
+			if uNode.Name == "" {
+				uNode, uerr = kubeClient.CoreV1().Nodes().Get(ctx, node.Name, metav1.GetOptions{})
+				if uerr != nil {
+					return uerr
+				}
+			}
+			node.Spec.DeepCopyInto(&uNode.Spec)
+			uNode.ObjectMeta.Labels = node.ObjectMeta.Labels
+			uNode.ObjectMeta.Annotations = node.ObjectMeta.Annotations
+			_, err := kubeClient.CoreV1().Nodes().Update(ctx, uNode, opts)
+			return err
+		}
+		return uerr
+	}
+	return nil
+}
+
+func tryUpdatingTaintWithExpectation(ctx context.Context, t *testing.T, clientSet *k8sclient.Clientset, node *corev1.Node, taint *corev1.Taint, add, expectedSuccess bool) {
+	rnode, err := clientSet.CoreV1().Nodes().Get(ctx, node.Name, metav1.GetOptions{})
+	if err != nil {
+		t.Fatalf("Failed refreshing node: %v", err)
+	}
+	var tNode *corev1.Node
+	var updated bool
+	var tErr error
+	var taintOperation string
+	if add {
+		tNode, updated, tErr = taints.AddOrUpdateTaint(rnode, taint)
+		if tErr != nil {
+			t.Fatalf("Failed applying taint: %v", tErr)
+		}
+		taintOperation = "apply"
+	} else {
+		tNode, updated, tErr = taints.RemoveTaint(rnode, taint)
+		if tErr != nil {
+			t.Fatalf("Failed removing taint: %v", tErr)
+		}
+		taintOperation = "remove"
+	}
+	if updated {
+		uerr := updateNodeAndRetryOnConflicts(ctx, clientSet, tNode, metav1.UpdateOptions{})
+		if expectedSuccess {
+			if uerr != nil {
+				t.Fatalf("Failed trying to %v taint to node: %v: %v", taintOperation, tNode.Name, uerr)
+			}
+		} else {
+			expectedErr := fmt.Sprintf(
+				"is forbidden: ValidatingAdmissionPolicy '%v' with binding '%v' denied request: User system:serviceaccount:%v:%v is",
+				softTainterValidatingAdmissionPolicyName,
+				softTainterValidatingAdmissionPolicyBindingName,
+				operatorclient.OperatorNamespace,
+				softTainterServiceAccountName)
+			if uerr == nil {
+				t.Fatalf("softtaint SA was allowed to %v taint %v, %v to node: %v", taintOperation, taint.Key, taint.Effect, tNode.Name)
+			} else if !strings.Contains(uerr.Error(), expectedErr) {
+				t.Fatalf("unexpected error: %v", uerr)
+			}
+		}
+	} else {
+		t.Fatalf("trying to %v taint %v, %v on/from node %v produces no changes", taintOperation, taint.Key, taint.Effect, tNode.Name)
+	}
+}
+
+func tryAddingTaintWithExpectedSuccess(ctx context.Context, t *testing.T, clientSet *k8sclient.Clientset, node *corev1.Node, taint *corev1.Taint) {
+	tryUpdatingTaintWithExpectation(ctx, t, clientSet, node, taint, true, true)
+}
+
+func tryAddingTaintWithExpectedFailure(ctx context.Context, t *testing.T, clientSet *k8sclient.Clientset, node *corev1.Node, taint *corev1.Taint) {
+	tryUpdatingTaintWithExpectation(ctx, t, clientSet, node, taint, true, false)
+}
+
+func tryRemovingTaintWithExpectedSuccess(ctx context.Context, t *testing.T, clientSet *k8sclient.Clientset, node *corev1.Node, taint *corev1.Taint) {
+	tryUpdatingTaintWithExpectation(ctx, t, clientSet, node, taint, false, true)
+}
+
+func tryRemovingTaintWithExpectedFailure(ctx context.Context, t *testing.T, clientSet *k8sclient.Clientset, node *corev1.Node, taint *corev1.Taint) {
+	tryUpdatingTaintWithExpectation(ctx, t, clientSet, node, taint, false, false)
+}


Reviews:
user: login: ingvagabund
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T09:17:49Z
user: login: tiraboschi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T09:39:44Z
user: login: tiraboschi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T09:39:51Z
user: login: ricardomaraschini
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T10:51:26Z

Review comments:
diff_hunk: @@ -172,37 +199,10 @@ func (st *softTainter) Reconcile(ctx context.Context, request reconcile.Request)
 
 	st.resourceNames = getResourceNames(st.args.thresholds)
 
-	nodeSelector := labels.Everything()
-	if policy.NodeSelector != nil {
-		sel, err := labels.Parse(*policy.NodeSelector)
-		if err != nil {
-			return reconcile.Result{}, err
-		}
-		nodeSelector = sel
-	}
-
-	nl := corev1.NodeList{}
-	err = st.client.List(ctx, &nl, &client.ListOptions{LabelSelector: nodeSelector})
+	err = st.syncSoftTaints(ctx, nodeList)
 	if err != nil {
 		return reconcile.Result{}, err
 	}
-	nodeList := make([]*corev1.Node, len(nl.Items))
-	for i, node := range nl.Items {
-		nodeList[i] = &node
-	}
-
-	if st.args.applySoftTaints {
path: pkg/softtainter/softtainter.go
user: login: ingvagabund
body: `applySoftTaints` is no longer needed in that case
created_at: 2025-04-10T08:50:30Z
author_association: MEMBER
diff_hunk: @@ -33,8 +37,22 @@ import (
 	"github.com/openshift/library-go/pkg/operator/resource/resourceread"
 )
 
+const (
+	kubeVirtLabelKey                                   = "kubevirt.io/schedulable"
+	kubeVirtLabelValue                                 = "true"
+	workersLabelSelector                               = "node-role.kubernetes.io/worker="
+	softTainterDeploymentName                          = "softtainter"
+	softTainterServiceAccountName                      = "openshift-descheduler-softtainter"
+	softTainterClusterRoleName                         = "openshift-descheduler-softtainter"
+	softTainterClusterRoleBindingName                  = "openshift-descheduler-softtainter"
+	softTainterClusterMonitoringViewClusterRoleBinding = "openshift-descheduler-softtainter-monitoring"
+	softTainterValidatingAdmissionPolicyName           = "openshift-descheduler-softtainter-vap"
+	softTainterValidatingAdmissionPolicyBindingName    = "openshift-descheduler-softtainter-vap-binding"
+)
+
 var operatorConfigs = map[string]string{
-	"base": "assets/07_descheduler-operator.cr.yaml",
+	"base":                         "assets/07_descheduler-operator.cr.yaml",
path: test/e2e/operator_test.go
user: login: ingvagabund
body: Moving "base" and "devKubeVirtRelieveAndMigrate" under const will avoid making typos.
created_at: 2025-04-10T09:15:31Z
author_association: MEMBER
diff_hunk: @@ -172,37 +199,10 @@ func (st *softTainter) Reconcile(ctx context.Context, request reconcile.Request)
 
 	st.resourceNames = getResourceNames(st.args.thresholds)
 
-	nodeSelector := labels.Everything()
-	if policy.NodeSelector != nil {
-		sel, err := labels.Parse(*policy.NodeSelector)
-		if err != nil {
-			return reconcile.Result{}, err
-		}
-		nodeSelector = sel
-	}
-
-	nl := corev1.NodeList{}
-	err = st.client.List(ctx, &nl, &client.ListOptions{LabelSelector: nodeSelector})
+	err = st.syncSoftTaints(ctx, nodeList)
 	if err != nil {
 		return reconcile.Result{}, err
 	}
-	nodeList := make([]*corev1.Node, len(nl.Items))
-	for i, node := range nl.Items {
-		nodeList[i] = &node
-	}
-
-	if st.args.applySoftTaints {
path: pkg/softtainter/softtainter.go
user: login: tiraboschi
body: done
created_at: 2025-04-10T09:39:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,8 +37,22 @@ import (
 	"github.com/openshift/library-go/pkg/operator/resource/resourceread"
 )
 
+const (
+	kubeVirtLabelKey                                   = "kubevirt.io/schedulable"
+	kubeVirtLabelValue                                 = "true"
+	workersLabelSelector                               = "node-role.kubernetes.io/worker="
+	softTainterDeploymentName                          = "softtainter"
+	softTainterServiceAccountName                      = "openshift-descheduler-softtainter"
+	softTainterClusterRoleName                         = "openshift-descheduler-softtainter"
+	softTainterClusterRoleBindingName                  = "openshift-descheduler-softtainter"
+	softTainterClusterMonitoringViewClusterRoleBinding = "openshift-descheduler-softtainter-monitoring"
+	softTainterValidatingAdmissionPolicyName           = "openshift-descheduler-softtainter-vap"
+	softTainterValidatingAdmissionPolicyBindingName    = "openshift-descheduler-softtainter-vap-binding"
+)
+
 var operatorConfigs = map[string]string{
-	"base": "assets/07_descheduler-operator.cr.yaml",
+	"base":                         "assets/07_descheduler-operator.cr.yaml",
path: test/e2e/operator_test.go
user: login: tiraboschi
body: done
created_at: 2025-04-10T09:39:50Z
author_association: CONTRIBUTOR

PR comments:
user: login: ricardomaraschini
created_at: 2025-04-10T10:51:41Z
body: /lgtm
author_association: CONTRIBUTOR
user: login: ingvagabund
created_at: 2025-04-10T10:56:41Z
body: /approve
author_association: MEMBER
user: login: openshift-ci[bot]
created_at: 2025-04-10T10:56:57Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/openshift/cluster-kube-descheduler-operator/pull/495#issuecomment-2792357985" title="Approved">ingvagabund</a>*, *<a href="https://github.com/openshift/cluster-kube-descheduler-operator/pull/495#issuecomment-2792344687" title="LGTM">ricardomaraschini</a>*, *<a href="https://github.com/openshift/cluster-kube-descheduler-operator/pull/495#" title="Author self-approved">tiraboschi</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=openshift%2Fcluster-kube-descheduler-operator).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/openshift/cluster-kube-descheduler-operator/blob/master/OWNERS)~~ [ingvagabund]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: CONTRIBUTOR
user: login: openshift-ci[bot]
created_at: 2025-04-10T11:06:30Z
body: @tiraboschi: all tests **passed!**


[Full PR test history](https://prow.ci.openshift.org/pr-history?org=openshift&repo=cluster-kube-descheduler-operator&pr=495). [Your PR dashboard](https://prow.ci.openshift.org/pr?query=is:pr+state:open+author:tiraboschi).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
author_association: CONTRIBUTOR