Stop createOrUpdate from rewriting objects it watches on every reconcile - #71
Conversation
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Repository YAML (base), Central YAML (inherited) Review profile: CHILL Plan: Pro Plus Run ID: 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 |
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: aswinsuryan The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:
Approvers can indicate their approval by writing |
|
I took this for a spin on a 4.22.9 AWS cluster with route servers up, and it does what it says for two of the three kinds -- CUDN and RouteAdvertisements both stop writing once they've converged. FRRConfiguration doesn't, and I think that's worth sorting before this lands. FRRConfiguration still gets written on every pass
The unit tests can't catch it because the fake client doesn't apply CRD defaults. It's less alarming than it sounds -- this isn't the old loop coming back. I watched it for five minutes: three write attempts, one reconcile, resourceVersion never budged. The API server discards an update that changes nothing, so you're paying one wasted The way out is to compare only the fields you actually set, rather than insisting the whole spec matches. This passes your tests as they stand, is lint clean, and still catches drift in func specEqual(existing, desired *unstructured.Unstructured) bool {
existingSpec, _, _ := unstructured.NestedMap(existing.Object, "spec")
desiredSpec, _, _ := unstructured.NestedMap(desired.Object, "spec")
return specSatisfied(existingSpec, desiredSpec)
}
// specSatisfied reports whether everything we set is already present with the
// value we want. Fields we never set are ignored, because the API server adds
// its own: FRRConfiguration defaults neighbors[].dualStackAddressFamily, so an
// exact comparison is false on every pass and we rewrite forever.
func specSatisfied(existing, desired interface{}) bool {
switch want := desired.(type) {
case map[string]interface{}:
have, ok := existing.(map[string]interface{})
if !ok {
return false
}
for k, v := range want {
if !specSatisfied(have[k], v) {
return false
}
}
return true
case []interface{}:
have, ok := existing.([]interface{})
if !ok || len(have) != len(want) {
return false
}
for i := range want {
if !specSatisfied(have[i], want[i]) {
return false
}
}
return true
default:
return apiequality.Semantic.DeepEqual(existing, desired)
}
}The downside is that a field you never set, which somebody then edits by hand, won't be reverted any more. Since you only ever send a partial spec I'd argue that's the behaviour you want regardless. Server-side apply with a field owner would do the same job more neatly, but that's a bigger change than belongs in this PR. The update path takes other people's labels and annotations with it
Because of the problem above, that's happening today. I put a label and an annotation on Fixing the first problem hides this most of the time, but it doesn't go away -- any genuine spec change brings it back. And it isn't only labels: the CUDN carries
obj.SetLabels(mergeLabels(existing.GetLabels(), obj.GetLabels()))Annotations want the same treatment. On the testsThey're good and they earn their keep -- pull the guard out and all three regression tests go red, drop One aside while I was in there: the API server warns that |
createOrUpdate did an unconditional Get-then-Update on every call, and both controllers watch the objects they write, so every write re-triggered their own reconcile - an infinite loop (already found and fixed once, in unmerged upstream PR openshift#13, but never merged to main). It also fanned out further than that: CUDNBgpConfig's reconcile makes live AWS calls, and CUDNBgpRouting's shared RouteAdvertisements object turns one CR's rewrite into a reconcile storm across every CR. createOrUpdate now skips the write when the spec and our own managed labels already match, while leaving any labels we don't own untouched. Comparing the spec exactly doesn't work: FRRConfiguration defaults neighbors[].dualStackAddressFamily, so what comes back from the API server always has a field we never set, and an exact comparison would be false forever, rewriting the object on every single reconcile. The comparison now only checks that the fields we set are present with the value we want, and ignores anything only the server added. The write also replaced metadata wholesale, so anything we don't manage ourselves - a foreign label, or ovn-kubernetes' own finalizer or annotations on a CUDN - was silently dropped the first time a real update happened. Labels and annotations from the existing object are now merged in before the update instead of being overwritten. Adds direct unit tests for createOrUpdate/specEqual/labelsSatisfied and regression tests in both controllers' reconcile tests. The envtest integration suite from the first pass of this work is left out for now and will follow separately. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Aswin Suryanarayanan <asuryana@redhat.com>
f959b86 to
6245948
Compare
|
@aswinsuryan: all tests passed! Full PR test history. Your PR dashboard. DetailsInstructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here. |
Thanks for the review, fixed both. specEqual now only checks that the fields we set are present with the value we want, instead of exact matching the whole spec, so FRRConfiguration's server defaulted dualStackAddressFamily no longer forces an update on every reconcile. Labels and annotations from the existing object are now merged in before Update, so anything we don't manage, like a foreign label or ovn-kubernetes finalizer or annotations, survives a real update instead of being wiped by the wholesale metadata replace. |
|
Some measurements from a live cluster, in case they help move this along. Reproduced on a 4.22.10 IPI AWS cluster in us-east-2, three availability zones, one
So this is not a slow drift. It is the operator writing at whatever rate the API server will serve, indefinitely, while reporting itself healthy. Nothing fails, which is presumably why it has gone unnoticed for so long. On the fix: the subset comparison is the part that matters, and it is worth saying why, because an exact #76 targets the same loop with an exact comparison, so these two want deciding between rather than both merging. The one piece there that this PR does not cover is |
createOrUpdate did an unconditional Get-then-Update on every call, and both controllers watch the objects they write, so every write re-triggered their own reconcile - an infinite loop (already found and fixed once, in unmerged upstream PR #13, but never merged to main). It also fanned out further than that: CUDNBgpConfig's reconcile makes live AWS calls, and CUDNBgpRouting's shared RouteAdvertisements object turns one CR's rewrite into a reconcile storm across every CR.
createOrUpdate now skips the write when the spec and our own managed labels already match, while leaving any labels we don't own untouched.
Adds direct unit tests for createOrUpdate/specEqual/labelsSatisfied and regression tests in both controllers' reconcile tests. The envtest integration suite from the first pass of this work is left out for now and will follow separately.