fix(controller): forward APIReader+CircuitBreaker through NamespaceReconciler

H4: NamespaceReconciler.reconcileMirror builds an ad-hoc SourceReconciler
to delegate mirror creation. The previous version left APIReader and
CircuitBreaker as nil, which silently disabled freshness verification
on the namespace-driven path (a label change to a target namespace
would mirror cached, possibly stale source data) and bypassed circuit
breaker accounting for those reconciles.

Construction extracted into newSourceReconciler so the forwarding is
covered by a unit test that pins both fields by identity.
This commit is contained in:
2026-05-02 22:41:11 +01:00
parent dfe08b35d1
commit a8e48a9eb6
3 changed files with 56 additions and 8 deletions
@@ -17,6 +17,7 @@ import (
ctrl "sigs.k8s.io/controller-runtime"
"github.com/lukaszraczylo/kubemirror/pkg/circuitbreaker"
"github.com/lukaszraczylo/kubemirror/pkg/config"
"github.com/lukaszraczylo/kubemirror/pkg/constants"
"github.com/lukaszraczylo/kubemirror/pkg/filter"
@@ -237,6 +238,40 @@ func TestNamespaceReconciler_CleanupWhenNamespaceNoLongerTarget(t *testing.T) {
})
}
}
func TestNamespaceReconciler_newSourceReconciler_forwardsAPIReaderAndCircuitBreaker(t *testing.T) {
// Regression test (H4): the SourceReconciler that NamespaceReconciler builds
// for delegated mirror reconciliation must carry the APIReader and the
// CircuitBreaker, otherwise namespace-driven mirror updates silently bypass
// --verify-source-freshness and the per-resource failure throttling.
apiReader := &stubAPIReader{}
cb := circuitbreaker.NewWithDefaults()
r := &NamespaceReconciler{
APIReader: apiReader,
CircuitBreaker: cb,
Config: &config.Config{},
}
gvk := schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Secret"}
sr := r.newSourceReconciler(gvk)
require.NotNil(t, sr)
assert.Same(t, apiReader, sr.APIReader, "APIReader must be forwarded")
assert.Same(t, cb, sr.CircuitBreaker, "CircuitBreaker must be forwarded")
assert.Equal(t, gvk, sr.GVK)
}
// stubAPIReader is a minimal client.Reader for identity-comparison tests; it
// is never invoked, so the methods only need to satisfy the interface.
type stubAPIReader struct{}
func (s *stubAPIReader) Get(_ context.Context, _ client.ObjectKey, _ client.Object, _ ...client.GetOption) error {
return nil
}
func (s *stubAPIReader) List(_ context.Context, _ client.ObjectList, _ ...client.ListOption) error {
return nil
}
// Helper functions