fix: the informer can not register use GetInformer instead#5931
fix: the informer can not register use GetInformer instead#5931Skarlso merged 10 commits intoexternal-secrets:mainfrom
Conversation
WalkthroughAdds generic-target validation and content-hash annotation, short-circuits reconcile when no refresh needed, switches informer lookup to use Unstructured (preserving GVK), conditionally registers informers, centralizes manifest templating return flow, updates createSimpleManifest signature, and adds informer/manifest unit tests. Changes
Possibly related PRs
🚥 Pre-merge checks | ✅ 1 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. No actionable comments were generated in the recent review. 🎉 Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
813222d to
a2f27b1
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
pkg/controllers/externalsecret/informer_manager.go (1)
250-257:⚠️ Potential issue | 🟠 MajorMatch
RemoveInformerobject type toGetInformer: useUnstructuredconsistently.The informer is created via
GetInformer(ctx, &unstructured.Unstructured{})at line 119, but removed viaRemoveInformer(ctx, &metav1.PartialObjectMetadata{})at line 254. Controller-runtime's cache implementation routes removals based on the object type, soRemoveInformerwill look for the informer in thePartialObjectMetadatamap rather than theUnstructuredmap where it was registered. This causes the removal to fail silently, leaking the informer resource.Proposed fix
if len(entry.externalSecrets) == 0 { - partial := &metav1.PartialObjectMetadata{} - partial.SetGroupVersionKind(gvk) + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(gvk) - if err := m.cache.RemoveInformer(ctx, partial); err != nil { + if err := m.cache.RemoveInformer(ctx, obj); err != nil { m.log.Error(err, "failed to remove informer, will clean up tracking anyway", "gvk", key) }
🤖 Fix all issues with AI agents
In `@pkg/controllers/externalsecret/externalsecret_controller.go`:
- Around line 278-281: The generic-target path currently returns early when
shouldRefresh(externalSecret) is false and thus skips both validity checks and
informer registration, creating drift gaps; modify the generic flow in
reconcileGenericTarget (or the caller that uses shouldRefresh) to either 1) add
an equivalent validity check like isSecretValid(existingSecret, externalSecret)
before skipping—lookup the existingSecret for the generic target and only return
when both !shouldRefresh(...) && isSecretValid(... )—or 2) ensure the informer
registration logic currently inside reconcileGenericTarget is executed
regardless of shouldRefresh so informers are registered on first reconcile
(move/registerInformer call out of the refresh-only branch); update
getRequeueResult usage accordingly so requeue behavior remains identical.
🧹 Nitpick comments (2)
pkg/controllers/externalsecret/informer_manager_test.go (2)
51-65:fakeCachedoesn't mockRemoveInformer, leaving the full-release path untested.When the last ExternalSecret is released,
ReleaseInformercallsm.cache.RemoveInformer(ctx, partial). SincefakeCacheembeds a nilruntimecache.Cache, this would panic if exercised. Consider adding aRemoveInformermock and a test that releases all ESs for a GVK to verify the informer is fully cleaned up:Suggested additions
type fakeCache struct { runtimecache.Cache getInformerCalled bool getInformerObj client.Object getInformerErr error + removeInformerCalled bool + removeInformerObj client.Object + removeInformerErr error } + +func (f *fakeCache) RemoveInformer(ctx context.Context, obj client.Object) error { + f.removeInformerCalled = true + f.removeInformerObj = obj + return f.removeInformerErr +}Then add a test that ensures releasing all ESs triggers
RemoveInformerand clears the entry from theinformersmap.
165-191: Partial release test is correct but consider also testing full cleanup.This test verifies partial release (one of two ESs removed). As noted above, a test for releasing the last ES would exercise the
RemoveInformeranddelete(m.informers, key)path, and verifyIsManagedreturnsfalseafterward.
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
pkg/controllers/externalsecret/informer_manager.go (1)
250-257:⚠️ Potential issue | 🔴 CriticalUse
Unstructuredinstead ofPartialObjectMetadatawhen removing informers — the current mismatch causes silent cleanup failure and resource leaks.The informer is registered in
EnsureInformerwith&unstructured.Unstructured{}(line 117–119), butReleaseInformerattempts removal with&metav1.PartialObjectMetadata{}(line 251). controller-runtime's cache maintains separate internal maps for each object "flavor" (structured, unstructured, metadata). SinceRemoveInformeronly removes from the map corresponding to the object type passed, removal withPartialObjectMetadatasilently succeeds without actually stopping the unstructured informer—the tracking entry is deleted, but the underlying informer keeps running and consuming resources.Fix
if len(entry.externalSecrets) == 0 { - partial := &metav1.PartialObjectMetadata{} - partial.SetGroupVersionKind(gvk) - - if err := m.cache.RemoveInformer(ctx, partial); err != nil { + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(gvk) + + if err := m.cache.RemoveInformer(ctx, obj); err != nil { m.log.Error(err, "failed to remove informer, will clean up tracking anyway", "gvk", key) }
Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com> On-behalf-of: Gergely Brautigam <gergely.brautigam@sap.com>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
pkg/controllers/externalsecret/externalsecret_controller.go (1)
658-673:⚠️ Potential issue | 🔴 CriticalNil pointer dereference in
isGenericTargetValidwhen target resource is deleted externally.When
getGenericResourcereturns(nil, notFoundErr)for a missing target (line 663), theisGenericTargetValidfunction is called withexisting=nilat line 670. The function immediately dereferencesexistingTargetatGetUID()without a null check, causing a panic.This is reachable: after a controller restart where the ExternalSecret has a recent sync (
shouldRefresh→ false) but the target resource has been deleted externally.🔧 Fix — add nil guard before calling `isGenericTargetValid`
- if !shouldRefresh(externalSecret) && isGenericTargetValid(existing, externalSecret) { + if !shouldRefresh(externalSecret) && existing != nil && isGenericTargetValid(existing, externalSecret) { log.V(1).Info("skipping refresh of generic target") return r.getRequeueResult(externalSecret), nil }Alternatively, add the nil guard inside
isGenericTargetValidbefore callingGetUID().
🤖 Fix all issues with AI agents
In `@pkg/controllers/externalsecret/externalsecret_controller.go`:
- Around line 1181-1205: The function isGenericTargetValid should defensively
handle a nil existingTarget: add a nil guard at the start of
isGenericTargetValid(existingTarget *unstructured.Unstructured, es
*esv1.ExternalSecret) that returns false if existingTarget == nil (unless
CreationPolicy==CreatePolicyOrphan), to avoid NPEs when callers pass nil;
additionally re-evaluate the TODO by computing a data-hash for unstructured
targets (e.g., hash existingTarget.Object or a specific sub-path like ".data" or
".spec") and compare against esv1.AnnotationDataHash using the same hashing
helper (esutils.ObjectHash or equivalent) to restore drift detection for
isGenericTargetValid.
Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@pkg/controllers/externalsecret/externalsecret_controller.go`:
- Around line 1181-1200: The code crashes when getGenericResource returns nil
for a not-found resource and isGenericTargetValid is called with
existingTarget==nil; add a nil guard to avoid dereferencing: update
isGenericTargetValid(existingTarget *unstructured.Unstructured, es
*esv1.ExternalSecret) to immediately return false if existingTarget == nil (so
callers like the callsite after getGenericResource no longer panic), keeping the
existing checks on GetUID, labels (esv1.LabelManaged), and annotation
(esv1.AnnotationDataHash vs genericTargetContentHash) unchanged.
- Around line 1202-1214: The default branch of genericTargetContentHash hashes
the whole obj.Object (including metadata.annotations), causing a mismatch after
applyTemplateToManifest writes AnnotationDataHash; change
genericTargetContentHash so that in the fallback it hashes a copy of the object
with metadata (or at minimum metadata.annotations) removed before calling
esutils.ObjectHash. Update genericTargetContentHash (referenced by
isGenericTargetValid and applyTemplateToManifest) to create a shallow/deep copy
of content, delete copy["metadata"] or copy["metadata"].(annotations) and then
compute esutils.ObjectHash on that sanitized map so the pre-annotation and
stored-annotation hashes match.
🧹 Nitpick comments (1)
pkg/controllers/externalsecret/externalsecret_controller.go (1)
658-673: Provider data is fetched before the validity/refresh check, unlike the Secret path.In the Secret reconciliation path (line 354),
shouldRefreshandisSecretValidare evaluated before callingGetProviderSecretData(line 399), avoiding unnecessary provider calls when the target is already valid. Here,GetProviderSecretDataruns at line 620, well before the validity check at line 670. This means every reconciliation triggers a provider call even when the generic target is valid and no refresh is needed.Consider moving the existing-resource fetch and validity check above
GetProviderSecretData, or at least before the provider call, to avoid unnecessary external calls on every requeue.
Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
pkg/controllers/externalsecret/externalsecret_controller_manifest_test.go (1)
284-337:⚠️ Potential issue | 🟡 MinorAdd test coverage for
isGenericTargetValidandgenericTargetContentHasherror paths.The functions
isGenericTargetValid(line 1188) andgenericTargetContentHash(line 1215) inexternalsecret_controller.golack test coverage. Specifically:
genericTargetContentHashreturns an error when an object has neitherspecnordatafields, but this error path is not tested.isGenericTargetValiddoes not validate null pointers and callsgenericTargetContentHash, but neither function is tested in this file.Add tests for:
genericTargetContentHashwith objects containingspec,data, and neither (expecting an error).isGenericTargetValidwith nil/empty targets, valid targets, and mismatched hashes.
🧹 Nitpick comments (3)
pkg/controllers/externalsecret/externalsecret_controller.go (3)
670-679: Validity check placement meansGetProviderSecretDatais called even when skipping refresh.
GetProviderSecretData(line 620) is called unconditionally before the validity/refresh check at line 676. In the Secret path (line 354),shouldRefreshis checked before fetching provider data. This means generic targets always call the provider even when skipping, adding latency and unnecessary API calls.Consider moving the validity/refresh check before
GetProviderSecretData, or at least before the provider call. The existing resource fetch (lines 658-668) would need to happen before the check, which it already does.♻️ Suggested restructure sketch
func (r *Reconciler) reconcileGenericTarget(...) (ctrl.Result, error) { - // retrieve the provider secret data - dataMap, err := r.GetProviderSecretData(ctx, externalSecret) - if err != nil { ... } - - // if no data was found, handle it according to deletion policy - if len(dataMap) == 0 { ... } - // Check if we need to fetch existing resource first existing := &unstructured.Unstructured{} if externalSecret.Spec.Target.CreationPolicy == esv1.CreatePolicyMerge || ... { existing, getErr = r.getGenericResource(ctx, log, externalSecret) ... } valid, err := isGenericTargetValid(existing, externalSecret) ... if !shouldRefresh(externalSecret) && valid { log.V(1).Info("skipping refresh of generic target") return r.getRequeueResult(externalSecret), nil } + // retrieve the provider secret data + dataMap, err := r.GetProviderSecretData(ctx, externalSecret) + if err != nil { ... } + + // if no data was found, handle it according to deletion policy + if len(dataMap) == 0 { ... } + // render the template for the manifest ...
734-743: Informer registered even forCreatePolicyNone.For
CreatePolicyNone(line 697), no resource is created or updated, yet the informer is still registered at line 737. This is unnecessary work — there's nothing to watch for drift detection. Consider gating the informer registration to only run after an actual create/update.
658-668:existinginitialization is effectively overwritten for Merge/Orphan/Owner, leaving stale empty object for other policies.Line 658 initializes
existingas an empty&unstructured.Unstructured{}. ForCreatePolicyNone, this empty object flows intoisGenericTargetValid(line 670) whereGetUID() == ""returns(false, nil). This works correctly by accident — the empty unstructured has no UID. But the intent would be clearer ifexistingwere initialized toniland the nil guard were added toisGenericTargetValid(as noted in the other comment).
Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
pkg/controllers/externalsecret/externalsecret_controller_manifest.go (1)
261-265:⚠️ Potential issue | 🟡 MinorUnchecked type assertion can panic.
If
obj.Object["spec"]exists but is notmap[string]any(e.g., set to a different type by a prior code path or an unusual existing object), line 264 will panic.Proposed fix — use a checked assertion
- if obj.Object["spec"] == nil { - obj.Object["spec"] = make(map[string]any) - } - spec := obj.Object["spec"].(map[string]any) - spec["data"] = data + if obj.Object["spec"] == nil { + obj.Object["spec"] = make(map[string]any) + } + spec, ok := obj.Object["spec"].(map[string]any) + if !ok { + obj.Object["spec"] = map[string]any{"data": data} + return obj + } + spec["data"] = data
🤖 Fix all issues with AI agents
In `@pkg/controllers/externalsecret/externalsecret_controller_manifest.go`:
- Around line 232-235: The error message uses es.Spec.Target.Name which may be
empty; update the error return in the genericTargetContentHash error branch to
use getTargetName(es) instead (i.e., replace es.Spec.Target.Name with
getTargetName(es)) so the logged target identifier is consistent and non-empty;
locate the code around the call to genericTargetContentHash(result) in
externalsecret_controller_manifest.go and change the fmt.Errorf call
accordingly.
| hash, err := genericTargetContentHash(result) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to hash target %q content: %w", es.Spec.Target.Name, err) | ||
| } |
There was a problem hiding this comment.
Error message may print an empty target name.
es.Spec.Target.Name can be empty when the name falls back to es.Name (via getTargetName). Consider using getTargetName(es) here for a consistent, non-empty identifier in the error message.
Proposed fix
- hash, err := genericTargetContentHash(result)
- if err != nil {
- return nil, fmt.Errorf("failed to hash target %q content: %w", es.Spec.Target.Name, err)
+ hash, err := genericTargetContentHash(result)
+ if err != nil {
+ return nil, fmt.Errorf("failed to hash target %q content: %w", getTargetName(es), err)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| hash, err := genericTargetContentHash(result) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to hash target %q content: %w", es.Spec.Target.Name, err) | |
| } | |
| hash, err := genericTargetContentHash(result) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to hash target %q content: %w", getTargetName(es), err) | |
| } |
🤖 Prompt for AI Agents
In `@pkg/controllers/externalsecret/externalsecret_controller_manifest.go` around
lines 232 - 235, The error message uses es.Spec.Target.Name which may be empty;
update the error return in the genericTargetContentHash error branch to use
getTargetName(es) instead (i.e., replace es.Spec.Target.Name with
getTargetName(es)) so the logged target identifier is consistent and non-empty;
locate the code around the call to genericTargetContentHash(result) in
externalsecret_controller_manifest.go and change the fmt.Errorf call
accordingly.
Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
c0a1cee to
a7ec76c
Compare
…ched Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
|
…secrets#5931) Signed-off-by: Nattapong Ekudomsuk <nuttapong_mos@hotmail.com>



Problem Statement
The Informer for Kind did not work for other CRDs, let's just use the proper informer registration for it.
Related Issue
Fixes #5587
Proposed Changes
How do you like to solve the issue and why?
Format
Please ensure that your PR follows the following format for the title:
Where
scopeis optionally one of:Checklist
git commit --signoffmake testmake reviewableSummary
Switch informer registration to use cache.GetInformer with an unstructured.Unstructured (obj.SetGroupVersionKind(gvk)) so informers work for arbitrary CRDs. Add pre-emptive generic-target validation and content-hash checks to short-circuit reconciliation when a managed target is up-to-date. Gate informer lifecycle and lazy registration by target CreationPolicy and AllowGenericTargets. Unify manifest/templating flow to a single return path, annotate manifests with a data/spec content hash, and change createSimpleManifest to return *unstructured.Unstructured (removed error return). Add unit tests for the informer manager and for generic-target hashing/validation.
Key changes
pkg/controllers/externalsecret/informer_manager.go
pkg/controllers/externalsecret/externalsecret_controller.go
pkg/controllers/externalsecret/externalsecret_controller_manifest.go
Tests
Addresses issue: #5587 — fixes informer registration for CRDs and improves generic-target templating/manifest handling.
Notes: contributor guidance and PR checklist present but some items (contribution guidelines read, commits signed, tests added/passing, readiness) are unchecked.