Repository navigation
fix(component): stop reporting writes by other controllers as updates - #249
Conversation
A component compared the whole object before and after its apply. When another controller added a finalizer, a label or an annotation between the read and the apply, the component recorded an Updated<Kind> event for a change it did not make. The comparison now covers only the fields that the field manager of the component owns before or after the apply, as recorded in managedFields. A changed, added or removed owned field still counts as an update. When the read carries no managedFields, the whole object is compared as before. Fixes #248 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Managed-field comparison misses version-converted removals and associative-list reordering while misclassifying foreign additions to empty granular maps, leading to incorrect change detection and update events/counts.
3 open issues
What changed in this PR
This PR addresses #248 by classifying component updates using fields owned by the component’s field manager, rather than counting unrelated writes from other controllers.
Changes:
- Compares owned fields before and after apply, retaining whole-object comparison when ownership information is unavailable.
- Adds unit and integration coverage for concurrent writes, owned-field changes, and removals.
- Updates public documentation and generated plugin references to explain the classification behavior.
| File | Description |
|---|---|
plugin/skills/custom-resource-wrappers/references/custom-resource.md |
Syncs wrapper guidance on apply classification. |
plugin/skills/building-components/references/component.md |
Syncs owned-field and fallback documentation. |
pkg/component/create.go |
Passes the field manager into change detection. |
pkg/component/create_test.go |
Runs classification cases with and without managed fields. |
pkg/component/concepts/converging.go |
Documents owned-field operation semantics. |
pkg/component/applied_change.go |
Implements ownership-based comparison and fallback. |
pkg/component/applied_change_test.go |
Tests concurrent writes, removals, and field-path lookup. |
pkg/component/applied_change_integration_test.go |
Tests Deployment classification against a real API server. |
go.mod |
Promotes structured-merge-diff to a direct dependency. |
docs/custom-resource.md |
Explains operation classification for resource wrappers. |
docs/component.md |
Documents owned-field comparison and missing-ownership fallback. |
Review effort: Balanced
A lookup by key finds a list item wherever it sits, so the owned-field
comparison reported None when the component only changed the order of
its containers. The order of some lists, such as init containers,
changes what the object does. The comparison now also compares the
relative order of the owned items of each list. Items that only other
managers own do not count.
The paths of a managedFields entry are expressed in the API version of
that entry. When the entries of the component before and after the
apply have different versions, a conversion can rename a field, and a
path from the old version finds nothing. The comparison now falls back
to the whole object in that case.
The docs now state both rules, and that a write which sets a field
inside an empty object the component applies, such as emptyDir: {},
still counts as a change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 1Review base: The Copilot review had 3 inline findings and no suppressed comments. Two are fixed: the reorder of owned list items, and the fallback when the entry versions differ. One is declined, with the reason in its thread: an empty object owned as one leaf. The docs now state that limit. Doc comments that grewThe comment gate flagged 2 doc comments, and
The sentence marked "by instruction" documents the limit of the declined finding, as the PR author asked. Proposals that were not applied, because they are about text from before this review and no finding asks for them:
Removed blocks: the doc of |

Description
Fixes #248
A component recorded an
Updated<Kind>event when another controller wrote to the object between the read and the apply of the component. This PR compares only the fields that the field manager of the component owns, so a finalizer, label or annotation from another controller is no longer an update. If the read carries nomanagedFields, for example with a cache that strips them, the whole object is compared as before, and this race still gives a falseUpdated. The race also still gives a falseUpdatedwhen the other controller sets a field inside an empty object that the component applies, such asemptyDir: {}.Changes
Updated<Kind>event, anupdatedapply count, orConvergingOperationUpdatedto the status handler.Updated<Kind>event.ConvergingOperationGoDoc,docs/component.mdanddocs/custom-resource.mdnow state the owned-field rule, the cases that fall back to the whole object, and the empty-object limit.Testing
make allpasses. The new unit tests use the fake client withWithReturnManagedFieldsand an interceptor that writes as another field manager right after the read. They cover a finalizer, a label and an annotation from another manager (no event), an owned change during such a write (one event), and an owned removal (one event). The existing classification tests now run twice: withmanagedFieldsand without them.A new envtest spec does the same race against a real API server with a Deployment. It adds a finalizer as another manager, then changes the image and the container port. This exercises the keyed list paths in
managedFields. A second spec swaps two containers and expects oneUpdated. Against the old whole-object comparison, the race spec and the race unit tests fail. A comparison that uses only the owned fields after the apply makes the removal test fail. Unit tests also cover the order check (a swap of owned items counts, an item of another manager does not) and the fallback for entries at different API versions.No E2E run: no E2E test asserts on these events, and the envtest spec covers the real API server.
🤖 Generated with Claude Code