@@ -10,6 +10,7 @@ import (
1010 "encoding/json"
1111 "errors"
1212 "os"
13+ "sync/atomic"
1314 "testing"
1415 "time"
1516
@@ -721,6 +722,139 @@ func TestReconcile_CSI_agentReadyNoCsiPodKeepsTaint(t *testing.T) {
721722 assert .True (t , hasTaint (fresh ))
722723}
723724
725+ func TestReconcile_CSI_agentReadyNoCsiPod_zeroCreationTimestampRequeuesSchedulingTimeout (t * testing.T ) {
726+ now := testNow ()
727+ node := & corev1.Node {
728+ ObjectMeta : metav1.ObjectMeta {
729+ Name : testNodeName ,
730+ CreationTimestamp : metav1.Time {}, // zero — defensive branch in reconcileAgentReadyWaitForCSI
731+ },
732+ Spec : corev1.NodeSpec {Taints : []corev1.Taint {untaint .AgentNotReadyTaint ()}},
733+ }
734+ agent := agentPod (testPodName , testPodNS , testNodeName , true , 1 * time .Minute , now )
735+ c := newFakeClient (t , node , agent )
736+ const scheduling = 7 * time .Minute
737+ r , _ := newReconciler (t , c , now , PolicyRemove , time .Minute , scheduling , true )
738+
739+ result , err := r .Reconcile (context .Background (), ctrl.Request {NamespacedName : types.NamespacedName {Name : testNodeName }})
740+ require .NoError (t , err )
741+ assert .Equal (t , scheduling , result .RequeueAfter )
742+
743+ fresh := & corev1.Node {}
744+ require .NoError (t , c .Get (context .Background (), types.NamespacedName {Name : testNodeName }, fresh ))
745+ require .True (t , hasTaint (fresh ))
746+ }
747+
748+ func TestReconcile_CSI_agentReadyNoCsiPod_schedulingTimeoutRemovesTaint (t * testing.T ) {
749+ now := testNow ()
750+ const scheduling = 5 * time .Minute
751+ node := taintedNode (testNodeName , scheduling + time .Minute , now )
752+ agent := agentPod (testPodName , testPodNS , testNodeName , true , 1 * time .Minute , now )
753+ c := newFakeClient (t , node , agent )
754+ r , _ := newReconciler (t , c , now , PolicyRemove , 10 * time .Minute , scheduling , true )
755+
756+ result , err := r .Reconcile (context .Background (), ctrl.Request {NamespacedName : types.NamespacedName {Name : testNodeName }})
757+ require .NoError (t , err )
758+ assert .Equal (t , ctrl.Result {}, result )
759+
760+ fresh := & corev1.Node {}
761+ require .NoError (t , c .Get (context .Background (), types.NamespacedName {Name : testNodeName }, fresh ))
762+ assert .False (t , hasTaint (fresh ))
763+ }
764+
765+ func TestReconcile_CSI_agentReadyCsiNotReady_noStartTimeRequeuesReadinessTimeout (t * testing.T ) {
766+ now := testNow ()
767+ node := taintedNode (testNodeName , 0 , now )
768+ agent := agentPod (testPodName , testPodNS , testNodeName , true , 1 * time .Minute , now )
769+ // not Ready, startedAgo 0 → no Status.StartTime → latestPodStartTime !ok branch
770+ csi := csiNodeServerPod ("csi-1" , testPodNS , testNodeName , false , 0 , now )
771+ c := newFakeClient (t , node , agent , csi )
772+ const readiness = 9 * time .Minute
773+ r , _ := newReconciler (t , c , now , PolicyRemove , readiness , time .Minute , true )
774+
775+ result , err := r .Reconcile (context .Background (), ctrl.Request {NamespacedName : types.NamespacedName {Name : testNodeName }})
776+ require .NoError (t , err )
777+ assert .Equal (t , readiness , result .RequeueAfter )
778+
779+ fresh := & corev1.Node {}
780+ require .NoError (t , c .Get (context .Background (), types.NamespacedName {Name : testNodeName }, fresh ))
781+ assert .True (t , hasTaint (fresh ))
782+ }
783+
784+ func TestReconcile_CSI_agentReadyCsiNotReady_readinessTimeoutRemovesTaint (t * testing.T ) {
785+ now := testNow ()
786+ node := taintedNode (testNodeName , 0 , now )
787+ agent := agentPod (testPodName , testPodNS , testNodeName , true , 1 * time .Minute , now )
788+ const readiness = time .Minute
789+ csi := csiNodeServerPod ("csi-1" , testPodNS , testNodeName , false , 2 * readiness , now )
790+ c := newFakeClient (t , node , agent , csi )
791+ r , _ := newReconciler (t , c , now , PolicyRemove , readiness , 5 * time .Minute , true )
792+
793+ result , err := r .Reconcile (context .Background (), ctrl.Request {NamespacedName : types.NamespacedName {Name : testNodeName }})
794+ require .NoError (t , err )
795+ assert .Equal (t , ctrl.Result {}, result )
796+
797+ fresh := & corev1.Node {}
798+ require .NoError (t , c .Get (context .Background (), types.NamespacedName {Name : testNodeName }, fresh ))
799+ assert .False (t , hasTaint (fresh ))
800+ }
801+
802+ func TestReconcile_CSI_listDriverPodsError (t * testing.T ) {
803+ now := testNow ()
804+ node := taintedNode (testNodeName , 0 , now )
805+ agent := agentPod (testPodName , testPodNS , testNodeName , true , 1 * time .Minute , now )
806+ base := newFakeClient (t , node , agent )
807+ var listCalls atomic.Int32
808+ c := interceptor .NewClient (base , interceptor.Funcs {
809+ List : func (ctx context.Context , cl client.WithWatch , list client.ObjectList , opts ... client.ListOption ) error {
810+ if listCalls .Add (1 ) == 2 {
811+ return errors .New ("simulated CSI pod list failure" )
812+ }
813+ return base .List (ctx , list , opts ... )
814+ },
815+ })
816+ r , _ := newReconciler (t , c , now , PolicyRemove , time .Minute , time .Minute , true )
817+
818+ _ , err := r .Reconcile (context .Background (), ctrl.Request {NamespacedName : types.NamespacedName {Name : testNodeName }})
819+ require .Error (t , err )
820+ assert .Contains (t , err .Error (), "failed to list CSI driver pods" )
821+ }
822+
823+ func TestReconcile_CSI_bothReadyConflictReturnsRequeue (t * testing.T ) {
824+ now := testNow ()
825+ node := taintedNode (testNodeName , 0 , now )
826+ agent := agentPod (testPodName , testPodNS , testNodeName , true , 1 * time .Minute , now )
827+ csi := csiNodeServerPod ("csi-1" , testPodNS , testNodeName , true , 1 * time .Minute , now )
828+ base := newFakeClient (t , node , agent , csi )
829+ c := interceptor .NewClient (base , interceptor.Funcs {
830+ Patch : func (context.Context , client.WithWatch , client.Object , client.Patch , ... client.PatchOption ) error {
831+ return apierrors .NewConflict (schema.GroupResource {Resource : "nodes" }, testNodeName , errors .New ("race" ))
832+ },
833+ })
834+ r , _ := newReconciler (t , c , now , PolicyRemove , time .Minute , time .Minute , true )
835+
836+ result , err := r .Reconcile (context .Background (), ctrl.Request {NamespacedName : types.NamespacedName {Name : testNodeName }})
837+ require .NoError (t , err )
838+ assert .Equal (t , conflictRequeueDelay , result .RequeueAfter )
839+ }
840+
841+ func TestReconcile_CSI_bothReadyPatchErrorBubbles (t * testing.T ) {
842+ now := testNow ()
843+ node := taintedNode (testNodeName , 0 , now )
844+ agent := agentPod (testPodName , testPodNS , testNodeName , true , 1 * time .Minute , now )
845+ csi := csiNodeServerPod ("csi-1" , testPodNS , testNodeName , true , 1 * time .Minute , now )
846+ base := newFakeClient (t , node , agent , csi )
847+ c := interceptor .NewClient (base , interceptor.Funcs {
848+ Patch : func (context.Context , client.WithWatch , client.Object , client.Patch , ... client.PatchOption ) error {
849+ return errors .New ("boom" )
850+ },
851+ })
852+ r , _ := newReconciler (t , c , now , PolicyRemove , time .Minute , time .Minute , true )
853+
854+ _ , err := r .Reconcile (context .Background (), ctrl.Request {NamespacedName : types.NamespacedName {Name : testNodeName }})
855+ require .Error (t , err )
856+ }
857+
724858func TestPodWatchPredicate_withCSI (t * testing.T ) {
725859 now := testNow ()
726860 r , _ := newReconciler (t , newFakeClient (t ), now , PolicyRemove , time .Minute , time .Minute , true )
0 commit comments