Skip to content

Commit 5d54569

Browse files
authored
Merge pull request #51 from planetscale/joem/debug-logs
feat: add debug logging to diagnose label sync issues [minor]
2 parents f8ba6b8 + c066204 commit 5d54569

File tree

3 files changed

+251
-30
lines changed

3 files changed

+251
-30
lines changed

controller.go

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
awsconfig "github.com/aws/aws-sdk-go-v2/config"
1212
"github.com/aws/aws-sdk-go-v2/service/ec2"
1313
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
14+
"github.com/go-logr/logr"
1415
gce "google.golang.org/api/compute/v1"
1516
corev1 "k8s.io/api/core/v1"
1617
ctrl "sigs.k8s.io/controller-runtime"
@@ -21,6 +22,7 @@ import (
2122

2223
type NodeLabelController struct {
2324
client.Client
25+
Logger logr.Logger
2426
EC2Client ec2Client
2527
GCEClient gceClient
2628

@@ -67,15 +69,37 @@ func (r *NodeLabelController) SetupWithManager(mgr ctrl.Manager) error {
6769
if !ok {
6870
return false
6971
}
70-
return shouldProcessNodeUpdate(oldNode, newNode, r.Labels, r.Annotations)
72+
73+
// Process if any monitored label/annotation changed
74+
if shouldProcessNodeUpdate(oldNode, newNode, r.Labels, r.Annotations) {
75+
r.Logger.V(1).Info("Update event: label changed", "node", newNode.Name)
76+
return true
77+
}
78+
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
85+
}
86+
87+
return false
7188
},
7289

7390
CreateFunc: func(e event.CreateEvent) bool {
7491
node, ok := e.Object.(*corev1.Node)
7592
if !ok {
7693
return false
7794
}
78-
return shouldProcessNodeCreate(node, r.Labels, r.Annotations)
95+
shouldProcess := shouldProcessNodeCreate(node, r.Labels, r.Annotations)
96+
r.Logger.V(1).Info("Create event",
97+
"node", node.Name,
98+
"shouldProcess", shouldProcess,
99+
"labels", node.Labels,
100+
"monitoredLabels", r.Labels,
101+
)
102+
return shouldProcess
79103
},
80104

81105
DeleteFunc: func(e event.DeleteEvent) bool {
@@ -164,7 +188,7 @@ func shouldProcessNodeCreate(node *corev1.Node, monitoredLabels, monitoredAnnota
164188
}
165189

166190
func (r *NodeLabelController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
167-
logger := ctrl.Log.WithName("reconcile").WithValues("node", req.NamespacedName)
191+
logger := r.Logger.WithValues("node", req.NamespacedName)
168192

169193
var node corev1.Node
170194
if err := r.Get(ctx, req.NamespacedName, &node); err != nil {
@@ -180,25 +204,32 @@ func (r *NodeLabelController) Reconcile(ctx context.Context, req ctrl.Request) (
180204

181205
// Create a map for tags to sync with the cloud provider
182206
tagsToSync := make(map[string]string)
207+
var notFoundLabels, notFoundAnnotations []string
183208

184209
// First collect labels (may be overwritten by annotations with same key)
185-
if node.Labels != nil {
186-
for _, k := range r.Labels {
187-
if value, exists := node.Labels[k]; exists {
188-
tagsToSync[k] = value
189-
}
210+
for _, k := range r.Labels {
211+
if value, exists := node.Labels[k]; exists {
212+
tagsToSync[k] = value
213+
} else {
214+
notFoundLabels = append(notFoundLabels, k)
190215
}
191216
}
192217

193218
// Then collect annotations (will overwrite labels with same key)
194-
if node.Annotations != nil {
195-
for _, k := range r.Annotations {
196-
if value, exists := node.Annotations[k]; exists {
197-
tagsToSync[k] = value
198-
}
219+
for _, k := range r.Annotations {
220+
if value, exists := node.Annotations[k]; exists {
221+
tagsToSync[k] = value
222+
} else {
223+
notFoundAnnotations = append(notFoundAnnotations, k)
199224
}
200225
}
201226

227+
logger.V(1).Info("Collected tags to sync",
228+
"tagsToSync", tagsToSync,
229+
"notFoundLabels", notFoundLabels,
230+
"notFoundAnnotations", notFoundAnnotations,
231+
)
232+
202233
var err error
203234
switch r.Cloud {
204235
case "aws":
@@ -208,11 +239,11 @@ func (r *NodeLabelController) Reconcile(ctx context.Context, req ctrl.Request) (
208239
}
209240

210241
if err != nil {
211-
logger.Error(err, "failed to sync tags")
242+
logger.Error(err, "failed to sync tags", "providerID", providerID)
212243
return ctrl.Result{}, err
213244
}
214245

215-
logger.Info("Successfully synced tags to cloud provider", "tags", tagsToSync)
246+
logger.Info("Successfully synced tags to cloud provider", "providerID", providerID, "tags", tagsToSync)
216247
return ctrl.Result{}, nil
217248
}
218249

controller_test.go

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/aws/aws-sdk-go-v2/aws"
99
"github.com/aws/aws-sdk-go-v2/service/ec2"
1010
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
11+
"github.com/go-logr/logr"
1112
"github.com/stretchr/testify/assert"
1213
"github.com/stretchr/testify/require"
1314
gce "google.golang.org/api/compute/v1"
@@ -184,6 +185,7 @@ func TestReconcileAWS(t *testing.T) {
184185

185186
r := &NodeLabelController{
186187
Client: k8s,
188+
Logger: logr.Discard(),
187189
Labels: tt.labelsToCopy,
188190
Annotations: tt.annotationsToCopy,
189191
Cloud: "aws",
@@ -315,6 +317,7 @@ func TestReconcileGCP(t *testing.T) {
315317

316318
r := &NodeLabelController{
317319
Client: k8s,
320+
Logger: logr.Discard(),
318321
Labels: tt.labelsToCopy,
319322
Annotations: tt.annotationsToCopy,
320323
Cloud: "gcp",
@@ -658,3 +661,184 @@ func createNode(config mockNode) *corev1.Node {
658661
},
659662
}
660663
}
664+
665+
// TestPredicateToReconcileFlow tests the full flow from event through predicate to reconcile.
666+
// This simulates what controller-runtime does: events are filtered by predicates, and only
667+
// if the predicate allows, reconcile is called.
668+
func TestPredicateToReconcileFlow(t *testing.T) {
669+
tests := []struct {
670+
name string
671+
monitoredLabels []string
672+
initialNode mockNode
673+
updatedNode *mockNode // nil means no update step
674+
expectReconcileOnCreate bool
675+
expectReconcileOnUpdate bool
676+
expectTagsCreated []string // tag keys we expect to be created
677+
}{
678+
{
679+
name: "node created without monitored labels then labels added",
680+
monitoredLabels: []string{"env", "team"},
681+
initialNode: mockNode{
682+
Name: "node1",
683+
Labels: map[string]string{"kubernetes.io/hostname": "node1"},
684+
ProviderID: "aws:///us-east-1a/i-1234567890abcdef0",
685+
},
686+
updatedNode: &mockNode{
687+
Name: "node1",
688+
Labels: map[string]string{"kubernetes.io/hostname": "node1", "env": "prod", "team": "platform"},
689+
ProviderID: "aws:///us-east-1a/i-1234567890abcdef0",
690+
},
691+
expectReconcileOnCreate: false, // no monitored labels yet
692+
expectReconcileOnUpdate: true, // monitored labels added
693+
expectTagsCreated: []string{"env", "team"},
694+
},
695+
{
696+
name: "node created with monitored labels already present",
697+
monitoredLabels: []string{"env"},
698+
initialNode: mockNode{
699+
Name: "node1",
700+
Labels: map[string]string{"env": "prod"},
701+
ProviderID: "aws:///us-east-1a/i-1234567890abcdef0",
702+
},
703+
updatedNode: nil,
704+
expectReconcileOnCreate: true,
705+
expectReconcileOnUpdate: false,
706+
expectTagsCreated: []string{"env"},
707+
},
708+
{
709+
name: "node created without labels then only some monitored labels added",
710+
monitoredLabels: []string{"env", "team", "region"},
711+
initialNode: mockNode{
712+
Name: "node1",
713+
Labels: map[string]string{},
714+
ProviderID: "aws:///us-east-1a/i-1234567890abcdef0",
715+
},
716+
updatedNode: &mockNode{
717+
Name: "node1",
718+
Labels: map[string]string{"env": "prod"}, // only env, not team or region
719+
ProviderID: "aws:///us-east-1a/i-1234567890abcdef0",
720+
},
721+
expectReconcileOnCreate: false,
722+
expectReconcileOnUpdate: true,
723+
expectTagsCreated: []string{"env"}, // only env should be synced
724+
},
725+
{
726+
name: "node update that does not change monitored labels triggers resync",
727+
monitoredLabels: []string{"env"},
728+
initialNode: mockNode{
729+
Name: "node1",
730+
Labels: map[string]string{"env": "prod"},
731+
ProviderID: "aws:///us-east-1a/i-1234567890abcdef0",
732+
},
733+
updatedNode: &mockNode{
734+
Name: "node1",
735+
Labels: map[string]string{"env": "prod", "unrelated": "change"},
736+
ProviderID: "aws:///us-east-1a/i-1234567890abcdef0",
737+
},
738+
expectReconcileOnCreate: true,
739+
expectReconcileOnUpdate: true, // resync: node has monitored labels
740+
expectTagsCreated: []string{"env"},
741+
},
742+
{
743+
name: "multiple labels added in single update",
744+
monitoredLabels: []string{"env", "team", "cost-center"},
745+
initialNode: mockNode{
746+
Name: "node1",
747+
Labels: map[string]string{},
748+
ProviderID: "aws:///us-east-1a/i-1234567890abcdef0",
749+
},
750+
updatedNode: &mockNode{
751+
Name: "node1",
752+
Labels: map[string]string{
753+
"env": "prod",
754+
"team": "platform",
755+
"cost-center": "12345",
756+
},
757+
ProviderID: "aws:///us-east-1a/i-1234567890abcdef0",
758+
},
759+
expectReconcileOnCreate: false,
760+
expectReconcileOnUpdate: true,
761+
expectTagsCreated: []string{"env", "team", "cost-center"},
762+
},
763+
}
764+
765+
for _, tt := range tests {
766+
t.Run(tt.name, func(t *testing.T) {
767+
scheme := runtime.NewScheme()
768+
require.NoError(t, corev1.AddToScheme(scheme))
769+
770+
// Start with initial node in the fake client
771+
k8s := fake.NewClientBuilder().
772+
WithScheme(scheme).
773+
WithObjects(createNode(tt.initialNode)).
774+
Build()
775+
776+
mock := &mockEC2Client{currentTags: []types.TagDescription{}}
777+
778+
controller := &NodeLabelController{
779+
Client: k8s,
780+
Logger: logr.Discard(),
781+
Labels: tt.monitoredLabels,
782+
Annotations: []string{},
783+
Cloud: "aws",
784+
EC2Client: mock,
785+
}
786+
787+
// Simulate CREATE event
788+
initialNodeObj := createNode(tt.initialNode)
789+
createAllowed := shouldProcessNodeCreate(initialNodeObj, tt.monitoredLabels, []string{})
790+
791+
assert.Equal(t, tt.expectReconcileOnCreate, createAllowed,
792+
"Create predicate returned unexpected result")
793+
794+
if createAllowed {
795+
_, err := controller.Reconcile(context.Background(), ctrl.Request{
796+
NamespacedName: client.ObjectKey{Name: tt.initialNode.Name},
797+
})
798+
require.NoError(t, err)
799+
800+
// Verify tags were created
801+
createdKeys := make([]string, 0, len(mock.createdTags))
802+
for _, tag := range mock.createdTags {
803+
createdKeys = append(createdKeys, aws.ToString(tag.Key))
804+
}
805+
assert.ElementsMatch(t, tt.expectTagsCreated, createdKeys,
806+
"Created tags don't match expected")
807+
}
808+
809+
// Simulate UPDATE event if provided
810+
if tt.updatedNode != nil {
811+
// Reset mock for update test
812+
mock.createdTags = nil
813+
mock.currentTags = []types.TagDescription{} // EC2 has no tags yet (simulating missed create)
814+
815+
// Update the node in the fake client
816+
updatedNodeObj := createNode(*tt.updatedNode)
817+
err := k8s.Update(context.Background(), updatedNodeObj)
818+
require.NoError(t, err)
819+
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{})
823+
824+
assert.Equal(t, tt.expectReconcileOnUpdate, updateAllowed,
825+
"Update predicate returned unexpected result")
826+
827+
if updateAllowed {
828+
_, err := controller.Reconcile(context.Background(), ctrl.Request{
829+
NamespacedName: client.ObjectKey{Name: tt.updatedNode.Name},
830+
})
831+
require.NoError(t, err)
832+
833+
// Verify tags were created
834+
createdKeys := make([]string, 0, len(mock.createdTags))
835+
for _, tag := range mock.createdTags {
836+
createdKeys = append(createdKeys, aws.ToString(tag.Key))
837+
}
838+
assert.ElementsMatch(t, tt.expectTagsCreated, createdKeys,
839+
"Created tags on update don't match expected")
840+
}
841+
}
842+
})
843+
}
844+
}

0 commit comments

Comments
 (0)