mirror of
https://github.com/lukaszraczylo/kubemirror.git
synced 2026-06-10 23:09:14 +00:00
fix(controller): stop self-triggered reconcile loops
C2: updateLastSyncStatus wrote the sync-status annotation on every
successful reconcile. Because the source's watch predicate is the
'enabled' label (server-side filter), that Update fires a watch event
that re-enters Reconcile. With reconciled/error counts varying across
cycles, the value differs each time, so the API server bumps RV and
the loop never quiesces. Now skips the Update when the value matches
the existing annotation.
C3: NamespaceReconciler's happy-path returned RequeueAfter=3s
unconditionally. Every namespace in the cluster re-reconciled every
3 seconds forever, generating constant List calls per source kind.
Now returns ctrl.Result{}; cache-staleness windows are handled by
the manager's resync period and source freshness verification.
This commit is contained in:
@@ -1103,3 +1103,55 @@ func TestSourceReconciler_Reconcile_RefusesBlacklistedSecret(t *testing.T) {
|
||||
mockLister.AssertNotCalled(t, "ListNamespacesWithLabels", mock.Anything)
|
||||
mockClient.AssertNotCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything)
|
||||
}
|
||||
func TestSourceReconciler_updateLastSyncStatus_skipsWhenUnchanged(t *testing.T) {
|
||||
// Regression test: re-running with the same reconciled/error counts must
|
||||
// NOT issue an Update call — otherwise every successful reconcile bumps
|
||||
// resourceVersion, fires a watch event, and re-enters Reconcile in a loop.
|
||||
mockClient := new(MockClient)
|
||||
|
||||
source := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Secret",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "test",
|
||||
"namespace": "default",
|
||||
"annotations": map[string]interface{}{
|
||||
constants.AnnotationSyncStatus: "reconciled:3,errors:0",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
r := &SourceReconciler{Client: mockClient}
|
||||
err := r.updateLastSyncStatus(context.Background(), source, source, 3, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
mockClient.AssertNotCalled(t, "Update", mock.Anything, mock.Anything, mock.Anything)
|
||||
}
|
||||
|
||||
func TestSourceReconciler_updateLastSyncStatus_writesWhenChanged(t *testing.T) {
|
||||
mockClient := new(MockClient)
|
||||
mockClient.On("Update", mock.Anything, mock.AnythingOfType("*unstructured.Unstructured"), mock.Anything).Return(nil)
|
||||
|
||||
source := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Secret",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "test",
|
||||
"namespace": "default",
|
||||
"annotations": map[string]interface{}{
|
||||
constants.AnnotationSyncStatus: "reconciled:2,errors:0",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
r := &SourceReconciler{Client: mockClient}
|
||||
err := r.updateLastSyncStatus(context.Background(), source, source, 3, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "reconciled:3,errors:0", source.GetAnnotations()[constants.AnnotationSyncStatus])
|
||||
mockClient.AssertCalled(t, "Update", mock.Anything, mock.Anything, mock.Anything)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user