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  }