fix(component): stop two owners of one object from taking each other's fields - #198
Conversation
Every owner of a kind applied with the field manager <Kind>/<component>, so two owners whose components render the same object shared one manager and each forced apply relinquished the other's fields wholesale. Both reported converged and neither ever saw a conflict. The manager is now <Kind>/<component>/<owner UID>, so each owner is a distinct manager to the API server. A second owner's apply carries a second controller reference, which the API server rejects, so that owner's component errors instead of silently taking the object. Closes #197 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015o2rQvAQ5vRtq57JBxdRfE
There was a problem hiding this comment.
🟡 Changes recommended
The updated SSA docs/skill text overstates that the second owner’s apply is always rejected (it depends on controller owner-ref being set), which could mislead users in Unowned()/scope-mismatch cases.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR fixes a Server-Side Apply (SSA) field-manager collision in pkg/component that allowed two owners of the same Kind to silently overwrite (“take”) each other’s managed fields when they rendered the same Kubernetes object, while both owners still reported convergence.
Changes:
- Make the SSA field manager per-owner by including the owner UID (
<Kind>/<component>/<owner UID>) via a shared helper (applyFieldOwner) on both normal reconcile and suspension apply paths. - Add envtest coverage to verify a second owner cannot silently take over a shared object and that repeated reconciles keep a single manager entry.
- Update primitives documentation (and synced plugin references) to describe the new manager format and upgrade behavior.
File summaries
| File | Description |
|---|---|
pkg/component/create.go |
Introduces applyFieldOwner and applies it consistently on reconcile + suspension apply paths to prevent manager sharing across owners. |
pkg/component/create_field_manager_test.go |
Adds envtest specs reproducing the multi-owner collision and asserting correct failure/manager behavior after the fix. |
docs/primitives.md |
Documents the new SSA field manager format and upgrade behavior. |
plugin/skills/using-primitives/references/primitives.md |
Synced copy of docs/primitives.md with the same SSA field manager documentation updates. |
plugin/skills/using-primitives/SKILL.md |
Updates the skill guidance to reflect the new SSA field manager format and behavior. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The rejection depends on the controller reference. For Unowned() resources, or where a scope mismatch prevents the owner reference, the second owner's forced apply still takes the fields it declares; the manager name then shows the contention but the framework does not detect it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015o2rQvAQ5vRtq57JBxdRfE
There was a problem hiding this comment.
🟢 Approval recommended
The change is well-scoped, consistently applied across code paths, and validated with targeted envtest coverage for the reported failure mode.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
pkg/component/create.go:202
- The GoDoc claims using the UID (instead of name) keeps the field manager "well under" the API server's 128-character limit, but component names are only validated as non-empty (no max length), so the full "//" string can still exceed 128 chars in edge cases. Suggest softening the wording to avoid implying a hard guarantee.
// applyFieldOwner returns the Server-Side Apply field manager for one component
// of one owner: "<Kind>/<component>/<owner UID>", for example
// "ExampleApp/web-interface/3d8a9d5e-1c2b-4f6e-9a7d-0b1c2d3e4f5a". The owner's
// UID is part of the name so that two owners of the same kind whose components
// render the same object are distinct managers to the API server. With a manager
// shared across owners, each owner's forced apply would relinquish the other's
// fields wholesale and neither would ever see a conflict. The UID rather than
// the owner's name keeps the manager well under the API server's 128-character
// limit and identical for cluster-scoped and namespaced owners.
- Files reviewed: 5/5 changed files
- Comments generated: 0 new
- Review effort level: Lite
…arantee Component names have no maximum length, so the manager can still exceed the API server's 128-character limit; the UID only adds a fixed 36. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015o2rQvAQ5vRtq57JBxdRfE
|
Review round 2 carried one suppressed finding, with no thread to reply in, so its disposition is recorded here.
|
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015o2rQvAQ5vRtq57JBxdRfE
There was a problem hiding this comment.
🟢 Approval recommended
The change is narrowly scoped, has targeted envtest coverage for the reported failure mode, and the remaining feedback is a minor doc wording refinement.
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 1
- Review effort level: Lite
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015o2rQvAQ5vRtq57JBxdRfE
There was a problem hiding this comment.
🟢 Approval recommended
The changes consistently apply the per-owner SSA field manager across relevant paths, include targeted envtest coverage for the regression, and update documentation (including synced plugin references) to match the new behavior.
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 0 new
- Review effort level: Lite
…acters Neither the owner kind nor the component name has a bounded length, so the readable <Kind>/<component>/<UID> manager could exceed the API server's limit and every apply would be rejected. When it would, the manager is now the hex-encoded SHA-256 of the readable form: 64 characters, deterministic, still distinct per owner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015o2rQvAQ5vRtq57JBxdRfE
There was a problem hiding this comment.
🟢 Approval recommended
The core behavior change is well-contained, consistently applied across paths, and is backed by both unit tests and envtest coverage with only a minor documentation-comment nit noted.
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 1
- Review effort level: Lite
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015o2rQvAQ5vRtq57JBxdRfE
There was a problem hiding this comment.
🟢 Approval recommended
The change addresses the documented SSA ownership bug with a deterministic, bounded field-manager scheme and is covered by both unit tests and envtest specs that reproduce and validate the fix.
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 0 new
- Review effort level: Lite
Description
Fixes #197
Two custom resources of one kind whose components render the same object silently took each other's fields on every reconcile, and both reported converged. The apply field manager was
<Kind>/<component>for every owner of a kind, so from the API server's point of view it was one manager rewriting its own fields and neither owner ever saw a conflict. The manager is now<Kind>/<component>/<owner UID>, so each owner is a distinct manager, and when the framework sets a controller reference (the default) the second owner's apply is rejected by the API server (it carries a secondcontroller: trueowner reference), surfacing as an error on that owner's component instead of a silent takeover. ForUnowned()resources or a scope mismatch, where no controller reference is set, the second owner's forced apply still takes its declared fields;managedFieldsthen names each owner but the framework does not detect the contention. On upgrade, fields owned by the old manager are adopted on the next apply through forced ownership; fields the old manager held that are no longer in the desired state stay under the old name and are not pruned.Changes
<Kind>/<component>/<owner UID>on both the reconcile and the suspension apply path, built by one helper (applyFieldOwner).docs/primitives.mddocuments the new manager format, why the owner is part of it, and the upgrade behaviour; the plugin skill references are synced.Related
Blockedwhen another owner already controls the object (the issue's second option) is not included; it is tracked in Block a resource whose live object is controlled by another owner #199. With per-owner managers the second owner now fails loudly where a controller reference is set, and Block a resource whose live object is controlled by another owner #199 covers theUnowned()/scope-mismatch cases and a condition instead of an apply error.Testing
New envtest specs in
pkg/component/create_field_manager_test.goreconcile two owners of the same kind against one ConfigMap. The specs were written first and failed on the unfixed code exactly as the issue describes: the second owner's reconcile succeeded and took the object. After the fix the second reconcile returns an error, the ConfigMap keeps the first owner's data and controller reference, and its only Apply manager isMockOperatorCRD/shared/<first owner UID>. A second spec checks that repeated reconciles of one owner keep a single manager, and a third applies with a 128-character component name and checks the API server accepts the 64-character hashed manager and reports it inmanagedFields.TestApplyFieldOwnerunit-tests the boundary: exactly 128 stays readable, 129 hashes, and hashed managers still differ per owner.make allpasses (fmt, lint, unit tests, scaffold and example tests, example builds). E2E was not run: nothing ine2e/orexamples/inspects managed fields, and the behaviour is exercised against a real API server by envtest.🤖 Generated with Claude Code
https://claude.ai/code/session_015o2rQvAQ5vRtq57JBxdRfE