feat(component): Unowned() resource option to suppress owner reference#159
Conversation
Add Unowned() ResourceOption for resources that must outlive their owner CR, such as backup records. The component creates and updates the resource normally but omits the controller owner reference, so Kubernetes GC does not cascade-delete it when the owner is removed. Explicit deletion paths (Delete(), DeleteWhen(), GatedBy() when disabled, and suspension's DeleteOnSuspend()) are unaffected — only GC is suppressed. The Unowned flag is propagated through the suspension path so that suspension mutations applied to an Unowned resource also skip setting the owner reference. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a new component.Unowned() ResourceOption to allow managed resources to be created/updated without a controller owner reference, preventing Kubernetes garbage collection on owner CR deletion. To ensure the option applies consistently, the PR threads per-resource options through the suspension path by carrying reconcileEntry instead of bare Resource.
Changes:
- Introduces
Unowned()option, addsUnownedto resolvedresourceOptions, and documents the option’s semantics. - Threads
reconcileEntrythrough suspension/application helpers so per-resource options (includingUnowned) are available during suspension. - Updates and adds tests to cover option resolution and end-to-end “no owner refs” behavior in both normal and suspension paths.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/component/suspend.go | Updates suspension flow to operate on []reconcileEntry so options can affect suspension apply. |
| pkg/component/suspend_test.go | Updates suspension tests for reconcileEntry and adds a new Unowned suspension-path test. |
| pkg/component/resource_options.go | Adds Unowned() option and resolves it into resourceOptions. |
| pkg/component/resource_options_test.go | Adds test cases validating Unowned resolution and composition with Auxiliary. |
| pkg/component/create.go | Threads skipOwnerRef/Unowned into apply/mutate path; skips setting controller owner ref when configured. |
| pkg/component/create_test.go | Updates apply tests for reconcileEntry and adds tests for Unowned behavior in mutate/reconcile. |
| pkg/component/component.go | Adjusts managed-resource selection for suspension to return []reconcileEntry. |
| docs/component.md | Documents Unowned() in the resource options table and adds semantic explanation. |
| if skipOwnerRef { | ||
| return false, nil | ||
| } |
There was a problem hiding this comment.
Fixed in f6cbbc8. Added obj.SetOwnerReferences(nil) before returning in the skipOwnerRef branch so any owner reference cached from a previous reconcile is cleared. SSA then removes the entry this field manager previously owned. Test added: "should clear a previously-cached owner reference when skipOwnerRef is true".
| | `component.ReadOnly()` | **Read-only**: fetched but never modified; health still contributes | | ||
| | `component.Delete()` / `component.DeleteWhen(cond)` | **Delete**: removed from the cluster (unconditionally, or when `cond` is true); does not contribute to health | | ||
| | `component.GatedBy(gate)` | Deletes the resource when the feature gate is disabled; managed when enabled | | ||
| | `component.Unowned()` | **Unowned**: created and updated normally, but no owner reference is set; not garbage-collected on owner CR deletion | |
There was a problem hiding this comment.
Fixed in f6cbbc8. Changed to "no controller owner reference is set" to match the implementation, which only skips ctrl.SetControllerReference.
…s wording When `Unowned()` is set and an owner reference was present from a previous reconcile (the DesiredObject pointer retains the server response), SSA would re-apply the cached reference. Clear it explicitly before patching so SSA removes any entry this field manager previously owned. Also corrects the resource-options table in docs/component.md: "no owner reference is set" → "no controller owner reference is set", matching the actual implementation which only skips `ctrl.SetControllerReference`. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| // owner ref set before Unowned() was added). Sending nil via SSA removes | ||
| // any entry this field manager previously owned. | ||
| obj.SetOwnerReferences(nil) | ||
| return false, nil |
There was a problem hiding this comment.
Fixed in 8819491. Replaced SetOwnerReferences(nil) with an in-place filter that removes only the entry whose UID matches the component owner, so owner references set by Mutate() for other objects are preserved. Test added: "should preserve ownerReferences to other objects when skipOwnerRef is true".
SetOwnerReferences(nil) cleared every owner reference on the object, including refs that Mutate() may have set for a different owner. Replace it with an in-place filter that removes only the entry whose UID matches the component owner, preserving any other owner references intact. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Description
Adds
Unowned(), a newResourceOptionfor resources that must outlive their owner CR. The component creates and updates the resource normally but does not set a controller owner reference, so Kubernetes will not garbage-collect it when the owner is deleted. The primary use case is CRs that should persist after the application CR is removed.Changes
Unowned()ResourceOptionconstructor andUnowned boolfield onresourceOptionsmutateResourceandapplyResourceaccept askipOwnerRef boolparameter; the suspension path (suspendResources,suspendResource,managedResources) was updated to carryreconcileEntryinstead of bareResourceso the flag flows correctly through suspension mutationsapplyResourcesupdated to accept[]reconcileEntryfor the same reasondocs/component.mdupdated with the new option in the resource registration table and a prose explanation of its semanticsChallenges
The
Unownedflag needed to reach the suspension path, which previously worked with[]Resourceand had no per-resource option access. The fix was to threadreconcileEntrythroughmanagedResources(),suspendResources,suspendResource, andapplyResources— mechanical but touching several signatures. Existing tests updated accordingly.Testing
All tests pass (
make allclean). New tests cover: option resolution (Unownedsets the flag, composes withAuxiliary),mutateResourcewithskipOwnerRef=truereturns no owner ref and does not report a scope skip,reconcileResourcesend-to-end confirms the created object has no owner references, andsuspendResourceconfirms the same for the suspension path.