k8s.io/apiserver@v0.31.1/pkg/endpoints/handlers/fieldmanager/equality.go (about) 1 /* 2 Copyright 2021 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package fieldmanager 18 19 import ( 20 "context" 21 "fmt" 22 "os" 23 "reflect" 24 "strconv" 25 "sync" 26 "time" 27 28 "k8s.io/apimachinery/pkg/api/equality" 29 "k8s.io/apimachinery/pkg/api/meta" 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 32 "k8s.io/apimachinery/pkg/conversion" 33 "k8s.io/apimachinery/pkg/runtime" 34 "k8s.io/apiserver/pkg/endpoints/metrics" 35 "k8s.io/klog/v2" 36 ) 37 38 var ( 39 avoidTimestampEqualities conversion.Equalities 40 initAvoidTimestampEqualities sync.Once 41 ) 42 43 func getAvoidTimestampEqualities() conversion.Equalities { 44 initAvoidTimestampEqualities.Do(func() { 45 if avoidNoopTimestampUpdatesString, exists := os.LookupEnv("KUBE_APISERVER_AVOID_NOOP_SSA_TIMESTAMP_UPDATES"); exists { 46 if ret, err := strconv.ParseBool(avoidNoopTimestampUpdatesString); err == nil && !ret { 47 // leave avoidTimestampEqualities empty. 48 return 49 } else { 50 klog.Errorf("failed to parse envar KUBE_APISERVER_AVOID_NOOP_SSA_TIMESTAMP_UPDATES: %v", err) 51 } 52 } 53 54 var eqs = equality.Semantic.Copy() 55 err := eqs.AddFuncs( 56 func(a, b metav1.ManagedFieldsEntry) bool { 57 // Two objects' managed fields are equivalent if, ignoring timestamp, 58 // the objects are deeply equal. 59 a.Time = nil 60 b.Time = nil 61 return reflect.DeepEqual(a, b) 62 }, 63 func(a, b unstructured.Unstructured) bool { 64 // Check if the managed fields are equal by converting to structured types and leveraging the above 65 // function, then, ignoring the managed fields, equality check the rest of the unstructured data. 66 if !avoidTimestampEqualities.DeepEqual(a.GetManagedFields(), b.GetManagedFields()) { 67 return false 68 } 69 return equalIgnoringValueAtPath(a.Object, b.Object, []string{"metadata", "managedFields"}) 70 }, 71 ) 72 73 if err != nil { 74 panic(fmt.Errorf("failed to instantiate semantic equalities: %w", err)) 75 } 76 77 avoidTimestampEqualities = eqs 78 }) 79 return avoidTimestampEqualities 80 } 81 82 func equalIgnoringValueAtPath(a, b any, path []string) bool { 83 if len(path) == 0 { // found the value to ignore 84 return true 85 } 86 aMap, aOk := a.(map[string]any) 87 bMap, bOk := b.(map[string]any) 88 if !aOk || !bOk { 89 // Can't traverse into non-maps, ignore 90 return true 91 } 92 if len(aMap) != len(bMap) { 93 return false 94 } 95 pathHead := path[0] 96 for k, aVal := range aMap { 97 bVal, ok := bMap[k] 98 if !ok { 99 return false 100 } 101 if k == pathHead { 102 if !equalIgnoringValueAtPath(aVal, bVal, path[1:]) { 103 return false 104 } 105 } else if !avoidTimestampEqualities.DeepEqual(aVal, bVal) { 106 return false 107 } 108 } 109 return true 110 } 111 112 // IgnoreManagedFieldsTimestampsTransformer reverts timestamp updates 113 // if the non-managed parts of the object are equivalent 114 func IgnoreManagedFieldsTimestampsTransformer( 115 _ context.Context, 116 newObj runtime.Object, 117 oldObj runtime.Object, 118 ) (res runtime.Object, err error) { 119 equalities := getAvoidTimestampEqualities() 120 if len(equalities.Equalities) == 0 { 121 return newObj, nil 122 } 123 124 outcome := "unequal_objects_fast" 125 start := time.Now() 126 err = nil 127 res = nil 128 129 defer func() { 130 if err != nil { 131 outcome = "error" 132 } 133 134 metrics.RecordTimestampComparisonLatency(outcome, time.Since(start)) 135 }() 136 137 // If managedFields modulo timestamps are unchanged 138 // and 139 // rest of object is unchanged 140 // then 141 // revert any changes to timestamps in managed fields 142 // (to prevent spurious ResourceVersion bump) 143 // 144 // Procecure: 145 // Do a quicker check to see if just managed fields modulo timestamps are 146 // unchanged. If so, then do the full, slower check. 147 // 148 // In most cases which actually update the object, the managed fields modulo 149 // timestamp check will fail, and we will be able to return early. 150 // 151 // In other cases, the managed fields may be exactly the same, 152 // except for timestamp, but the objects are the different. This is the 153 // slow path which checks the full object. 154 oldAccessor, err := meta.Accessor(oldObj) 155 if err != nil { 156 return nil, fmt.Errorf("failed to acquire accessor for oldObj: %v", err) 157 } 158 159 accessor, err := meta.Accessor(newObj) 160 if err != nil { 161 return nil, fmt.Errorf("failed to acquire accessor for newObj: %v", err) 162 } 163 164 oldManagedFields := oldAccessor.GetManagedFields() 165 newManagedFields := accessor.GetManagedFields() 166 167 if len(oldManagedFields) != len(newManagedFields) { 168 // Return early if any managed fields entry was added/removed. 169 // We want to retain user expectation that even if they write to a field 170 // whose value did not change, they will still result as the field 171 // manager at the end. 172 return newObj, nil 173 } else if len(newManagedFields) == 0 { 174 // This transformation only makes sense when managedFields are 175 // non-empty 176 return newObj, nil 177 } 178 179 // This transformation only makes sense if the managed fields has at least one 180 // changed timestamp; and are otherwise equal. Return early if there are no 181 // changed timestamps. 182 allTimesUnchanged := true 183 for i, e := range newManagedFields { 184 if !e.Time.Equal(oldManagedFields[i].Time) { 185 allTimesUnchanged = false 186 break 187 } 188 } 189 190 if allTimesUnchanged { 191 return newObj, nil 192 } 193 194 eqFn := equalities.DeepEqual 195 if _, ok := newObj.(*unstructured.Unstructured); ok { 196 // Use strict equality with unstructured 197 eqFn = equalities.DeepEqualWithNilDifferentFromEmpty 198 } 199 200 // This condition ensures the managed fields are always compared first. If 201 // this check fails, the if statement will short circuit. If the check 202 // succeeds the slow path is taken which compares entire objects. 203 if !eqFn(oldManagedFields, newManagedFields) { 204 return newObj, nil 205 } 206 207 if eqFn(newObj, oldObj) { 208 // Remove any changed timestamps, so that timestamp is not the only 209 // change seen by etcd. 210 // 211 // newManagedFields is known to be exactly pairwise equal to 212 // oldManagedFields except for timestamps. 213 // 214 // Simply replace possibly changed new timestamps with their old values. 215 for idx := 0; idx < len(oldManagedFields); idx++ { 216 newManagedFields[idx].Time = oldManagedFields[idx].Time 217 } 218 219 accessor.SetManagedFields(newManagedFields) 220 outcome = "equal_objects" 221 return newObj, nil 222 } 223 224 outcome = "unequal_objects_slow" 225 return newObj, nil 226 }