Change:
What happened: On a resource with managedFields populated, a no-op patch modified the serialization order of managedFields. What you expected to happen: No-op patches do not result in a write to etcd. How to reproduce it (as minimally and precisely as possible): contains: the initial ADDED event from kubectl MODIFIED events from kube-controller-manager one MODIFIED event corresponding to the kubectl patch (from resourceVersion 1704 in 1.txt to resourceVersion 1718 in 2.txt) shows the kubectl and kube-controller-manager managed field sets swapped order: second patch was a no-op ( is empty) Anything else we need to know?: This was easiest to reproduce with server-side apply enabled at 100% /assign
/sig api-machinery
/wg apply
Can you print the before/after fieldmanager? I can't quite figure out what the actual values are. The code that sorts these lists is located
updated to unified diff and attached:
If you explicitly set the field manager to something unique, does it happen every time?
hm, no, it's definitely an ordering swap
is not sorted properly, since should come before .
We seem to also sort it in a second place:
Theory: the first time it goes through, the time is accurate to the nanosecond; the second time, it's accurate to the second
Yeah OK I'm pretty sure that's it, both times are the same second. Serialization drops the fractional part.

Discussion:
import ( "fmt" "sort" <del> "time" </del> metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime"

// If we have more than the maximum, sort the update entries by time, oldest first. sort.Slice(updaters, func(i, j int) bool { <del> iTime, jTime, nTime := managed.Times()[updaters[i]], managed.Times()[updaters[j]], &metav1.Time{Time: time.Time{}} if iTime == nil { iTime = nTime </del> <ins> iTime, jTime, iSeconds, jSeconds := managed.Times()[updaters[i]], managed.Times()[updaters[j]], int64(0), int64(0) if iTime != nil { iSeconds = iTime.Unix() </ins> } <del> if jTime == nil { jTime = nTime </del> <ins> if jTime != nil { jSeconds = jTime.Unix() </ins> } <del> if !iTime.Equal(jTime) { return iTime.Before(jTime) </del> <ins> if iSeconds != jSeconds { return iSeconds < jSeconds </ins> } return updaters[i] < updaters[j] })

"encoding/json" "fmt" "sort" <del> "time" </del> "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

return p.Operation < q.Operation } <del> ntime := &metav1.Time{Time: time.Time{}} if p.Time == nil { p.Time = ntime </del> <ins> pSeconds, qSeconds := int64(0), int64(0) if p.Time != nil { pSeconds = p.Time.Unix() </ins> } <del> if q.Time == nil { q.Time = ntime </del> <ins> if q.Time != nil { qSeconds = q.Time.Unix() </ins> } <del> if !p.Time.Equal(q.Time) { return p.Time.Before(q.Time) </del> <ins> if pSeconds != qSeconds { return pSeconds < qSeconds </ins> } if p.Manager != q.Manager {

{Manager: "e", Operation: metav1.ManagedFieldsOperationUpdate, Time: parseTimeOrPanic("2003-01-01T01:00:00Z")}, }, }, <ins> { name: "sort drops nanoseconds", managedFields: []metav1.ManagedFieldsEntry{ {Manager: "c", Operation: metav1.ManagedFieldsOperationUpdate, Time: &metav1.Time{time.Date(2000, time.January, 0, 0, 0, 0, 1, time.UTC)}}, {Manager: "a", Operation: metav1.ManagedFieldsOperationUpdate, Time: &metav1.Time{time.Date(2000, time.January, 0, 0, 0, 0, 2, time.UTC)}}, {Manager: "b", Operation: metav1.ManagedFieldsOperationUpdate, Time: &metav1.Time{time.Date(2000, time.January, 0, 0, 0, 0, 3, time.UTC)}}, }, expected: []metav1.ManagedFieldsEntry{ {Manager: "a", Operation: metav1.ManagedFieldsOperationUpdate, Time: &metav1.Time{time.Date(2000, time.January, 0, 0, 0, 0, 2, time.UTC)}}, {Manager: "b", Operation: metav1.ManagedFieldsOperationUpdate, Time: &metav1.Time{time.Date(2000, time.January, 0, 0, 0, 0, 3, time.UTC)}}, {Manager: "c", Operation: metav1.ManagedFieldsOperationUpdate, Time: &metav1.Time{time.Date(2000, time.January, 0, 0, 0, 0, 1, time.UTC)}}, }, }, </ins> } for _, test := range tests {