@@ -57,10 +57,11 @@ func (m *mockGCEClient) SetLabels(ctx context.Context, project, zone, instance s
5757}
5858
5959type mockNode struct {
60- Name string
61- Labels map [string ]string
62- Annotations map [string ]string
63- ProviderID string
60+ Name string
61+ Labels map [string ]string
62+ Annotations map [string ]string
63+ ProviderID string
64+ ResourceVersion string
6465}
6566
6667func TestReconcileAWS (t * testing.T ) {
@@ -652,16 +653,33 @@ func TestSanitizeKeysForGCP(t *testing.T) {
652653func createNode (config mockNode ) * corev1.Node {
653654 return & corev1.Node {
654655 ObjectMeta : metav1.ObjectMeta {
655- Name : config .Name ,
656- Labels : config .Labels ,
657- Annotations : config .Annotations ,
656+ Name : config .Name ,
657+ Labels : config .Labels ,
658+ Annotations : config .Annotations ,
659+ ResourceVersion : config .ResourceVersion ,
658660 },
659661 Spec : corev1.NodeSpec {
660662 ProviderID : config .ProviderID ,
661663 },
662664 }
663665}
664666
667+ // simulateUpdatePredicate matches the actual predicate logic in SetupWithManager.
668+ // Returns true if the update event should trigger reconciliation.
669+ func simulateUpdatePredicate (oldNode , newNode * corev1.Node , monitoredLabels , monitoredAnnotations []string ) bool {
670+ // Process if any monitored label/annotation changed
671+ if shouldProcessNodeUpdate (oldNode , newNode , monitoredLabels , monitoredAnnotations ) {
672+ return true
673+ }
674+
675+ // During periodic resync (same ResourceVersion), allow if node has monitored labels
676+ if oldNode .ResourceVersion == newNode .ResourceVersion {
677+ return shouldProcessNodeCreate (newNode , monitoredLabels , monitoredAnnotations )
678+ }
679+
680+ return false
681+ }
682+
665683// TestPredicateToReconcileFlow tests the full flow from event through predicate to reconcile.
666684// This simulates what controller-runtime does: events are filtered by predicates, and only
667685// if the predicate allows, reconcile is called.
@@ -723,20 +741,41 @@ func TestPredicateToReconcileFlow(t *testing.T) {
723741 expectTagsCreated : []string {"env" }, // only env should be synced
724742 },
725743 {
726- name : "node update that does not change monitored labels triggers resync " ,
744+ name : "periodic resync with same ResourceVersion triggers reconcile " ,
727745 monitoredLabels : []string {"env" },
728746 initialNode : mockNode {
729- Name : "node1" ,
730- Labels : map [string ]string {"env" : "prod" },
731- ProviderID : "aws:///us-east-1a/i-1234567890abcdef0" ,
747+ Name : "node1" ,
748+ Labels : map [string ]string {"env" : "prod" },
749+ ProviderID : "aws:///us-east-1a/i-1234567890abcdef0" ,
750+ ResourceVersion : "12345" ,
732751 },
733752 updatedNode : & mockNode {
734- Name : "node1" ,
735- Labels : map [string ]string {"env" : "prod" , "unrelated" : "change" },
736- ProviderID : "aws:///us-east-1a/i-1234567890abcdef0" ,
753+ Name : "node1" ,
754+ Labels : map [string ]string {"env" : "prod" }, // same labels
755+ ProviderID : "aws:///us-east-1a/i-1234567890abcdef0" ,
756+ ResourceVersion : "12345" , // same ResourceVersion = true resync
737757 },
738758 expectReconcileOnCreate : true ,
739- expectReconcileOnUpdate : true , // resync: node has monitored labels
759+ expectReconcileOnUpdate : true , // true resync: same ResourceVersion
760+ expectTagsCreated : []string {"env" },
761+ },
762+ {
763+ name : "heartbeat update with different ResourceVersion does not trigger reconcile" ,
764+ monitoredLabels : []string {"env" },
765+ initialNode : mockNode {
766+ Name : "node1" ,
767+ Labels : map [string ]string {"env" : "prod" },
768+ ProviderID : "aws:///us-east-1a/i-1234567890abcdef0" ,
769+ ResourceVersion : "12345" ,
770+ },
771+ updatedNode : & mockNode {
772+ Name : "node1" ,
773+ Labels : map [string ]string {"env" : "prod" }, // same labels
774+ ProviderID : "aws:///us-east-1a/i-1234567890abcdef0" ,
775+ ResourceVersion : "12346" , // different ResourceVersion = real update, not resync
776+ },
777+ expectReconcileOnCreate : true ,
778+ expectReconcileOnUpdate : false , // NOT a resync, just heartbeat - skip
740779 expectTagsCreated : []string {"env" },
741780 },
742781 {
@@ -812,20 +851,25 @@ func TestPredicateToReconcileFlow(t *testing.T) {
812851 mock .createdTags = nil
813852 mock .currentTags = []types.TagDescription {} // EC2 has no tags yet (simulating missed create)
814853
815- // Update the node in the fake client
854+ // Create the updated node object directly (not through fake client)
855+ // to test predicate logic with explicit ResourceVersions
816856 updatedNodeObj := createNode (* tt .updatedNode )
817- err := k8s .Update (context .Background (), updatedNodeObj )
818- require .NoError (t , err )
819857
820- // Match the actual predicate logic: allow if labels changed OR if node has monitored labels (resync)
821- updateAllowed := shouldProcessNodeUpdate (initialNodeObj , updatedNodeObj , tt .monitoredLabels , []string {}) ||
822- shouldProcessNodeCreate (updatedNodeObj , tt .monitoredLabels , []string {})
858+ // Use the helper that matches actual predicate logic
859+ updateAllowed := simulateUpdatePredicate (initialNodeObj , updatedNodeObj , tt .monitoredLabels , []string {})
823860
824861 assert .Equal (t , tt .expectReconcileOnUpdate , updateAllowed ,
825862 "Update predicate returned unexpected result" )
826863
827864 if updateAllowed {
828- _ , err := controller .Reconcile (context .Background (), ctrl.Request {
865+ // For reconcile test, update the node in the fake client
866+ // (without explicit ResourceVersion to avoid conflicts)
867+ nodeForClient := createNode (* tt .updatedNode )
868+ nodeForClient .ResourceVersion = "" // let fake client manage this
869+ err := k8s .Update (context .Background (), nodeForClient )
870+ require .NoError (t , err )
871+
872+ _ , err = controller .Reconcile (context .Background (), ctrl.Request {
829873 NamespacedName : client.ObjectKey {Name : tt .updatedNode .Name },
830874 })
831875 require .NoError (t , err )
0 commit comments