Skip to content

Commit 0e4db4c

Browse files
committed
fix: too frequent cloud api updates
The previous release introduced debug logging and new logic to attempt to work around potential edge cases where node label updates were being missed. This resulted in too frequent cloud api updates on every node change such as regular node status updates. This could be observed in the logs by nodes having their tags resynced every ~5minutes. This commit attempts to distinguish between real node label updates and periodic cache resync events. The 4 hour cache sync period remains in place to catch any missed events.
1 parent a922251 commit 0e4db4c

File tree

3 files changed

+78
-30
lines changed

3 files changed

+78
-30
lines changed

controller.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,16 @@ func (r *NodeLabelController) SetupWithManager(mgr ctrl.Manager) error {
7676
return true
7777
}
7878

79-
// Also process if node has monitored labels (catches resync events).
80-
// During resync, old == new, so shouldProcessNodeUpdate returns false,
81-
// but we still want to reconcile to catch any missed events.
82-
if shouldProcessNodeCreate(newNode, r.Labels, r.Annotations) {
83-
r.Logger.V(1).Info("Update event: resync", "node", newNode.Name)
84-
return true
79+
// During periodic resync, controller-runtime emits Update events where
80+
// oldObj and newObj are identical (same ResourceVersion). Allow these
81+
// through for nodes with monitored labels to catch any missed events.
82+
// But filter out real updates (different ResourceVersion) where only
83+
// non-label fields changed (e.g., heartbeat, status updates).
84+
if oldNode.ResourceVersion == newNode.ResourceVersion {
85+
if shouldProcessNodeCreate(newNode, r.Labels, r.Annotations) {
86+
r.Logger.V(1).Info("Update event: periodic resync", "node", newNode.Name)
87+
return true
88+
}
8589
}
8690

8791
return false

controller_test.go

Lines changed: 66 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,11 @@ func (m *mockGCEClient) SetLabels(ctx context.Context, project, zone, instance s
5757
}
5858

5959
type 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

6667
func TestReconcileAWS(t *testing.T) {
@@ -652,16 +653,33 @@ func TestSanitizeKeysForGCP(t *testing.T) {
652653
func 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)

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
module github.com/planetscale/k8s-node-tagger
22

3-
go 1.24.0
3+
go 1.25.0
44

5-
toolchain go1.24.5
5+
toolchain go1.25.5
66

77
require (
88
github.com/aws/aws-sdk-go-v2 v1.36.5

0 commit comments

Comments
 (0)