mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 11:50:44 +00:00
Merge pull request #106388 from alexzielenski/ssa-ignore-nonsemantic-changes
Ignore non-semantic changes to objects
This commit is contained in:
commit
8bc12f24e6
@ -34,3 +34,14 @@ func EqualitiesOrDie(funcs ...interface{}) Equalities {
|
|||||||
}
|
}
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Performs a shallow copy of the equalities map
|
||||||
|
func (e Equalities) Copy() Equalities {
|
||||||
|
result := Equalities{reflect.Equalities{}}
|
||||||
|
|
||||||
|
for key, value := range e.Equalities {
|
||||||
|
result.Equalities[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
@ -100,7 +100,8 @@ func makeUsefulPanic(v reflect.Value) {
|
|||||||
// Tests for deep equality using reflected types. The map argument tracks
|
// Tests for deep equality using reflected types. The map argument tracks
|
||||||
// comparisons that have already been seen, which allows short circuiting on
|
// comparisons that have already been seen, which allows short circuiting on
|
||||||
// recursive types.
|
// recursive types.
|
||||||
func (e Equalities) deepValueEqual(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool {
|
// equateNilAndEmpty controls whether empty maps/slices are equivalent to nil
|
||||||
|
func (e Equalities) deepValueEqual(v1, v2 reflect.Value, visited map[visit]bool, equateNilAndEmpty bool, depth int) bool {
|
||||||
defer makeUsefulPanic(v1)
|
defer makeUsefulPanic(v1)
|
||||||
|
|
||||||
if !v1.IsValid() || !v2.IsValid() {
|
if !v1.IsValid() || !v2.IsValid() {
|
||||||
@ -150,17 +151,36 @@ func (e Equalities) deepValueEqual(v1, v2 reflect.Value, visited map[visit]bool,
|
|||||||
// We don't need to check length here because length is part of
|
// We don't need to check length here because length is part of
|
||||||
// an array's type, which has already been filtered for.
|
// an array's type, which has already been filtered for.
|
||||||
for i := 0; i < v1.Len(); i++ {
|
for i := 0; i < v1.Len(); i++ {
|
||||||
if !e.deepValueEqual(v1.Index(i), v2.Index(i), visited, depth+1) {
|
if !e.deepValueEqual(v1.Index(i), v2.Index(i), visited, equateNilAndEmpty, depth+1) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
case reflect.Slice:
|
case reflect.Slice:
|
||||||
if (v1.IsNil() || v1.Len() == 0) != (v2.IsNil() || v2.Len() == 0) {
|
if equateNilAndEmpty {
|
||||||
return false
|
if (v1.IsNil() || v1.Len() == 0) != (v2.IsNil() || v2.Len() == 0) {
|
||||||
}
|
return false
|
||||||
if v1.IsNil() || v1.Len() == 0 {
|
}
|
||||||
return true
|
|
||||||
|
if v1.IsNil() || v1.Len() == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if v1.IsNil() != v2.IsNil() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimize nil and empty cases
|
||||||
|
// Two lists that are BOTH nil are equal
|
||||||
|
// No need to check v2 is nil since v1.IsNil == v2.IsNil from above
|
||||||
|
if v1.IsNil() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two lists that are both empty and both non nil are equal
|
||||||
|
if v1.Len() == 0 || v2.Len() == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if v1.Len() != v2.Len() {
|
if v1.Len() != v2.Len() {
|
||||||
return false
|
return false
|
||||||
@ -169,7 +189,7 @@ func (e Equalities) deepValueEqual(v1, v2 reflect.Value, visited map[visit]bool,
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
for i := 0; i < v1.Len(); i++ {
|
for i := 0; i < v1.Len(); i++ {
|
||||||
if !e.deepValueEqual(v1.Index(i), v2.Index(i), visited, depth+1) {
|
if !e.deepValueEqual(v1.Index(i), v2.Index(i), visited, equateNilAndEmpty, depth+1) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -178,22 +198,40 @@ func (e Equalities) deepValueEqual(v1, v2 reflect.Value, visited map[visit]bool,
|
|||||||
if v1.IsNil() || v2.IsNil() {
|
if v1.IsNil() || v2.IsNil() {
|
||||||
return v1.IsNil() == v2.IsNil()
|
return v1.IsNil() == v2.IsNil()
|
||||||
}
|
}
|
||||||
return e.deepValueEqual(v1.Elem(), v2.Elem(), visited, depth+1)
|
return e.deepValueEqual(v1.Elem(), v2.Elem(), visited, equateNilAndEmpty, depth+1)
|
||||||
case reflect.Pointer:
|
case reflect.Ptr:
|
||||||
return e.deepValueEqual(v1.Elem(), v2.Elem(), visited, depth+1)
|
return e.deepValueEqual(v1.Elem(), v2.Elem(), visited, equateNilAndEmpty, depth+1)
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
for i, n := 0, v1.NumField(); i < n; i++ {
|
for i, n := 0, v1.NumField(); i < n; i++ {
|
||||||
if !e.deepValueEqual(v1.Field(i), v2.Field(i), visited, depth+1) {
|
if !e.deepValueEqual(v1.Field(i), v2.Field(i), visited, equateNilAndEmpty, depth+1) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
if (v1.IsNil() || v1.Len() == 0) != (v2.IsNil() || v2.Len() == 0) {
|
if equateNilAndEmpty {
|
||||||
return false
|
if (v1.IsNil() || v1.Len() == 0) != (v2.IsNil() || v2.Len() == 0) {
|
||||||
}
|
return false
|
||||||
if v1.IsNil() || v1.Len() == 0 {
|
}
|
||||||
return true
|
if v1.IsNil() || v1.Len() == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if v1.IsNil() != v2.IsNil() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimize nil and empty cases
|
||||||
|
// Two maps that are BOTH nil are equal
|
||||||
|
// No need to check v2 is nil since v1.IsNil == v2.IsNil from above
|
||||||
|
if v1.IsNil() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two maps that are both empty and both non nil are equal
|
||||||
|
if v1.Len() == 0 || v2.Len() == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if v1.Len() != v2.Len() {
|
if v1.Len() != v2.Len() {
|
||||||
return false
|
return false
|
||||||
@ -202,7 +240,7 @@ func (e Equalities) deepValueEqual(v1, v2 reflect.Value, visited map[visit]bool,
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
for _, k := range v1.MapKeys() {
|
for _, k := range v1.MapKeys() {
|
||||||
if !e.deepValueEqual(v1.MapIndex(k), v2.MapIndex(k), visited, depth+1) {
|
if !e.deepValueEqual(v1.MapIndex(k), v2.MapIndex(k), visited, equateNilAndEmpty, depth+1) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -232,6 +270,14 @@ func (e Equalities) deepValueEqual(v1, v2 reflect.Value, visited map[visit]bool,
|
|||||||
// Unexported field members cannot be compared and will cause an informative panic; you must add an Equality
|
// Unexported field members cannot be compared and will cause an informative panic; you must add an Equality
|
||||||
// function for these types.
|
// function for these types.
|
||||||
func (e Equalities) DeepEqual(a1, a2 interface{}) bool {
|
func (e Equalities) DeepEqual(a1, a2 interface{}) bool {
|
||||||
|
return e.deepEqual(a1, a2, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Equalities) DeepEqualWithNilDifferentFromEmpty(a1, a2 interface{}) bool {
|
||||||
|
return e.deepEqual(a1, a2, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Equalities) deepEqual(a1, a2 interface{}, equateNilAndEmpty bool) bool {
|
||||||
if a1 == nil || a2 == nil {
|
if a1 == nil || a2 == nil {
|
||||||
return a1 == a2
|
return a1 == a2
|
||||||
}
|
}
|
||||||
@ -240,7 +286,7 @@ func (e Equalities) DeepEqual(a1, a2 interface{}) bool {
|
|||||||
if v1.Type() != v2.Type() {
|
if v1.Type() != v2.Type() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return e.deepValueEqual(v1, v2, make(map[visit]bool), 0)
|
return e.deepValueEqual(v1, v2, make(map[visit]bool), equateNilAndEmpty, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e Equalities) deepValueDerive(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool {
|
func (e Equalities) deepValueDerive(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool {
|
||||||
|
@ -16,6 +16,10 @@ func TestEqualities(t *testing.T) {
|
|||||||
type Baz struct {
|
type Baz struct {
|
||||||
Y Bar
|
Y Bar
|
||||||
}
|
}
|
||||||
|
type Zap struct {
|
||||||
|
A []int
|
||||||
|
B map[string][]int
|
||||||
|
}
|
||||||
err := e.AddFuncs(
|
err := e.AddFuncs(
|
||||||
func(a, b int) bool {
|
func(a, b int) bool {
|
||||||
return a+1 == b
|
return a+1 == b
|
||||||
@ -32,10 +36,12 @@ func TestEqualities(t *testing.T) {
|
|||||||
X int
|
X int
|
||||||
}
|
}
|
||||||
|
|
||||||
table := []struct {
|
type Case struct {
|
||||||
a, b interface{}
|
a, b interface{}
|
||||||
equal bool
|
equal bool
|
||||||
}{
|
}
|
||||||
|
|
||||||
|
table := []Case{
|
||||||
{1, 2, true},
|
{1, 2, true},
|
||||||
{2, 1, false},
|
{2, 1, false},
|
||||||
{"foo", "fo", false},
|
{"foo", "fo", false},
|
||||||
@ -70,6 +76,26 @@ func TestEqualities(t *testing.T) {
|
|||||||
t.Errorf("Expected (%+v == %+v) == %v, but got %v", item.a, item.b, e, a)
|
t.Errorf("Expected (%+v == %+v) == %v, but got %v", item.a, item.b, e, a)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cases which hinge upon implicit nil/empty map/slice equality
|
||||||
|
implicitTable := []Case{
|
||||||
|
{map[string][]int{}, map[string][]int(nil), true},
|
||||||
|
{[]int{}, []int(nil), true},
|
||||||
|
{map[string][]int{"foo": nil}, map[string][]int{"foo": {}}, true},
|
||||||
|
{Zap{A: nil, B: map[string][]int{"foo": nil}}, Zap{A: []int{}, B: map[string][]int{"foo": {}}}, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range implicitTable {
|
||||||
|
if e, a := item.equal, e.DeepEqual(item.a, item.b); e != a {
|
||||||
|
t.Errorf("Expected (%+v == %+v) == %v, but got %v", item.a, item.b, e, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range implicitTable {
|
||||||
|
if e, a := !item.equal, e.DeepEqualWithNilDifferentFromEmpty(item.a, item.b); e != a {
|
||||||
|
t.Errorf("Expected (%+v == %+v) == %v, but got %v", item.a, item.b, e, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDerivatives(t *testing.T) {
|
func TestDerivatives(t *testing.T) {
|
||||||
|
@ -0,0 +1,180 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2021 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package fieldmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/api/equality"
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/conversion"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apiserver/pkg/endpoints/metrics"
|
||||||
|
"k8s.io/klog/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func determineAvoidNoopTimestampUpdatesEnabled() bool {
|
||||||
|
if avoidNoopTimestampUpdatesString, exists := os.LookupEnv("KUBE_APISERVER_AVOID_NOOP_SSA_TIMESTAMP_UPDATES"); exists {
|
||||||
|
if ret, err := strconv.ParseBool(avoidNoopTimestampUpdatesString); err == nil {
|
||||||
|
return ret
|
||||||
|
} else {
|
||||||
|
klog.Errorf("failed to parse envar KUBE_APISERVER_AVOID_NOOP_SSA_TIMESTAMP_UPDATES: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// enabled by default
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
avoidNoopTimestampUpdatesEnabled = determineAvoidNoopTimestampUpdatesEnabled()
|
||||||
|
)
|
||||||
|
|
||||||
|
var avoidTimestampEqualities = func() conversion.Equalities {
|
||||||
|
var eqs = equality.Semantic.Copy()
|
||||||
|
|
||||||
|
err := eqs.AddFunc(
|
||||||
|
func(a, b metav1.ManagedFieldsEntry) bool {
|
||||||
|
// Two objects' managed fields are equivalent if, ignoring timestamp,
|
||||||
|
// the objects are deeply equal.
|
||||||
|
a.Time = nil
|
||||||
|
b.Time = nil
|
||||||
|
return reflect.DeepEqual(a, b)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return eqs
|
||||||
|
}()
|
||||||
|
|
||||||
|
// IgnoreManagedFieldsTimestampsTransformer reverts timestamp updates
|
||||||
|
// if the non-managed parts of the object are equivalent
|
||||||
|
func IgnoreManagedFieldsTimestampsTransformer(
|
||||||
|
_ context.Context,
|
||||||
|
newObj runtime.Object,
|
||||||
|
oldObj runtime.Object,
|
||||||
|
) (res runtime.Object, err error) {
|
||||||
|
if !avoidNoopTimestampUpdatesEnabled {
|
||||||
|
return newObj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
outcome := "unequal_objects_fast"
|
||||||
|
start := time.Now()
|
||||||
|
err = nil
|
||||||
|
res = nil
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
outcome = "error"
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics.RecordTimestampComparisonLatency(outcome, time.Since(start))
|
||||||
|
}()
|
||||||
|
|
||||||
|
// If managedFields modulo timestamps are unchanged
|
||||||
|
// and
|
||||||
|
// rest of object is unchanged
|
||||||
|
// then
|
||||||
|
// revert any changes to timestamps in managed fields
|
||||||
|
// (to prevent spurious ResourceVersion bump)
|
||||||
|
//
|
||||||
|
// Procecure:
|
||||||
|
// Do a quicker check to see if just managed fields modulo timestamps are
|
||||||
|
// unchanged. If so, then do the full, slower check.
|
||||||
|
//
|
||||||
|
// In most cases which actually update the object, the managed fields modulo
|
||||||
|
// timestamp check will fail, and we will be able to return early.
|
||||||
|
//
|
||||||
|
// In other cases, the managed fields may be exactly the same,
|
||||||
|
// except for timestamp, but the objects are the different. This is the
|
||||||
|
// slow path which checks the full object.
|
||||||
|
oldAccessor, err := meta.Accessor(oldObj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to acquire accessor for oldObj: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
accessor, err := meta.Accessor(newObj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to acquire accessor for newObj: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
oldManagedFields := oldAccessor.GetManagedFields()
|
||||||
|
newManagedFields := accessor.GetManagedFields()
|
||||||
|
|
||||||
|
if len(oldManagedFields) != len(newManagedFields) {
|
||||||
|
// Return early if any managed fields entry was added/removed.
|
||||||
|
// We want to retain user expectation that even if they write to a field
|
||||||
|
// whose value did not change, they will still result as the field
|
||||||
|
// manager at the end.
|
||||||
|
return newObj, nil
|
||||||
|
} else if len(newManagedFields) == 0 {
|
||||||
|
// This transformation only makes sense when managedFields are
|
||||||
|
// non-empty
|
||||||
|
return newObj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This transformation only makes sense if the managed fields has at least one
|
||||||
|
// changed timestamp; and are otherwise equal. Return early if there are no
|
||||||
|
// changed timestamps.
|
||||||
|
allTimesUnchanged := true
|
||||||
|
for i, e := range newManagedFields {
|
||||||
|
if !e.Time.Equal(oldManagedFields[i].Time) {
|
||||||
|
allTimesUnchanged = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if allTimesUnchanged {
|
||||||
|
return newObj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This condition ensures the managed fields are always compared first. If
|
||||||
|
// this check fails, the if statement will short circuit. If the check
|
||||||
|
// succeeds the slow path is taken which compares entire objects.
|
||||||
|
if !avoidTimestampEqualities.DeepEqualWithNilDifferentFromEmpty(oldManagedFields, newManagedFields) {
|
||||||
|
return newObj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if avoidTimestampEqualities.DeepEqualWithNilDifferentFromEmpty(newObj, oldObj) {
|
||||||
|
// Remove any changed timestamps, so that timestamp is not the only
|
||||||
|
// change seen by etcd.
|
||||||
|
//
|
||||||
|
// newManagedFields is known to be exactly pairwise equal to
|
||||||
|
// oldManagedFields except for timestamps.
|
||||||
|
//
|
||||||
|
// Simply replace possibly changed new timestamps with their old values.
|
||||||
|
for idx := 0; idx < len(oldManagedFields); idx++ {
|
||||||
|
newManagedFields[idx].Time = oldManagedFields[idx].Time
|
||||||
|
}
|
||||||
|
|
||||||
|
accessor.SetManagedFields(newManagedFields)
|
||||||
|
outcome = "equal_objects"
|
||||||
|
return newObj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
outcome = "unequal_objects_slow"
|
||||||
|
return newObj, nil
|
||||||
|
}
|
@ -659,8 +659,13 @@ func (p *patcher) patchResource(ctx context.Context, scope *RequestScope) (runti
|
|||||||
return obj, nil
|
return obj, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
transformers := []rest.TransformFunc{p.applyPatch, p.applyAdmission, dedupOwnerReferencesTransformer}
|
||||||
|
if scope.FieldManager != nil {
|
||||||
|
transformers = append(transformers, fieldmanager.IgnoreManagedFieldsTimestampsTransformer)
|
||||||
|
}
|
||||||
|
|
||||||
wasCreated := false
|
wasCreated := false
|
||||||
p.updatedObjectInfo = rest.DefaultUpdatedObjectInfo(nil, p.applyPatch, p.applyAdmission, dedupOwnerReferencesTransformer)
|
p.updatedObjectInfo = rest.DefaultUpdatedObjectInfo(nil, transformers...)
|
||||||
requestFunc := func() (runtime.Object, error) {
|
requestFunc := func() (runtime.Object, error) {
|
||||||
// Pass in UpdateOptions to override UpdateStrategy.AllowUpdateOnCreate
|
// Pass in UpdateOptions to override UpdateStrategy.AllowUpdateOnCreate
|
||||||
options := patchToUpdateOptions(p.options)
|
options := patchToUpdateOptions(p.options)
|
||||||
|
@ -191,6 +191,15 @@ func UpdateResource(r rest.Updater, scope *RequestScope, admit admission.Interfa
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore changes that only affect managed fields
|
||||||
|
// timestamps. FieldManager can't know about changes
|
||||||
|
// like normalized fields, defaulted fields and other
|
||||||
|
// mutations.
|
||||||
|
// Only makes sense when SSA field manager is being used
|
||||||
|
if scope.FieldManager != nil {
|
||||||
|
transformers = append(transformers, fieldmanager.IgnoreManagedFieldsTimestampsTransformer)
|
||||||
|
}
|
||||||
|
|
||||||
createAuthorizerAttributes := authorizer.AttributesRecord{
|
createAuthorizerAttributes := authorizer.AttributesRecord{
|
||||||
User: userInfo,
|
User: userInfo,
|
||||||
ResourceRequest: true,
|
ResourceRequest: true,
|
||||||
|
@ -238,6 +238,18 @@ var (
|
|||||||
[]string{"source", "status"},
|
[]string{"source", "status"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
requestTimestampComparisonDuration = compbasemetrics.NewHistogramVec(
|
||||||
|
&compbasemetrics.HistogramOpts{
|
||||||
|
Name: "apiserver_request_timestamp_comparison_time",
|
||||||
|
Help: "Time taken for comparison of old vs new objects in UPDATE or PATCH requests",
|
||||||
|
Buckets: []float64{0.0001, 0.0003, 0.001, 0.003, 0.01, 0.03, 0.1, 0.3, 1.0, 5.0},
|
||||||
|
StabilityLevel: compbasemetrics.ALPHA,
|
||||||
|
},
|
||||||
|
// Path the code takes to reach a conclusion:
|
||||||
|
// i.e. unequalObjectsFast, unequalObjectsSlow, equalObjectsSlow
|
||||||
|
[]string{"code_path"},
|
||||||
|
)
|
||||||
|
|
||||||
metrics = []resettableCollector{
|
metrics = []resettableCollector{
|
||||||
deprecatedRequestGauge,
|
deprecatedRequestGauge,
|
||||||
requestCounter,
|
requestCounter,
|
||||||
@ -256,6 +268,7 @@ var (
|
|||||||
requestFilterDuration,
|
requestFilterDuration,
|
||||||
requestAbortsTotal,
|
requestAbortsTotal,
|
||||||
requestPostTimeoutTotal,
|
requestPostTimeoutTotal,
|
||||||
|
requestTimestampComparisonDuration,
|
||||||
}
|
}
|
||||||
|
|
||||||
// these are the valid request methods which we report in our metrics. Any other request methods
|
// these are the valid request methods which we report in our metrics. Any other request methods
|
||||||
@ -366,6 +379,10 @@ func RecordFilterLatency(ctx context.Context, name string, elapsed time.Duration
|
|||||||
requestFilterDuration.WithContext(ctx).WithLabelValues(name).Observe(elapsed.Seconds())
|
requestFilterDuration.WithContext(ctx).WithLabelValues(name).Observe(elapsed.Seconds())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RecordTimestampComparisonLatency(codePath string, elapsed time.Duration) {
|
||||||
|
requestTimestampComparisonDuration.WithLabelValues(codePath).Observe(elapsed.Seconds())
|
||||||
|
}
|
||||||
|
|
||||||
func RecordRequestPostTimeout(source string, status string) {
|
func RecordRequestPostTimeout(source string, status string) {
|
||||||
requestPostTimeoutTotal.WithLabelValues(source, status).Inc()
|
requestPostTimeoutTotal.WithLabelValues(source, status).Inc()
|
||||||
}
|
}
|
||||||
|
@ -248,6 +248,185 @@ func TestNoOpUpdateSameResourceVersion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getRV(obj runtime.Object) (string, error) {
|
||||||
|
acc, err := meta.Accessor(obj)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return acc.GetResourceVersion(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNoSemanticUpdateAppleSameResourceVersion makes sure that APPLY requests which makes no semantic changes
|
||||||
|
// will not change the resource version (no write to etcd is done)
|
||||||
|
//
|
||||||
|
// Some of the non-semantic changes are:
|
||||||
|
// - Applying an atomic struct that removes a default
|
||||||
|
// - Changing Quantity or other fields that are normalized
|
||||||
|
func TestNoSemanticUpdateApplySameResourceVersion(t *testing.T) {
|
||||||
|
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)()
|
||||||
|
|
||||||
|
client, closeFn := setup(t)
|
||||||
|
defer closeFn()
|
||||||
|
|
||||||
|
ssBytes := []byte(`{
|
||||||
|
"apiVersion": "apps/v1",
|
||||||
|
"kind": "StatefulSet",
|
||||||
|
"metadata": {
|
||||||
|
"name": "nginx",
|
||||||
|
"labels": {"app": "nginx"}
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"serviceName": "nginx",
|
||||||
|
"selector": { "matchLabels": {"app": "nginx"}},
|
||||||
|
"template": {
|
||||||
|
"metadata": {
|
||||||
|
"labels": {"app": "nginx"}
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"containers": [{
|
||||||
|
"name": "nginx",
|
||||||
|
"image": "nginx",
|
||||||
|
"resources": {
|
||||||
|
"limits": {"memory": "2048Mi"}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"volumeClaimTemplates": [{
|
||||||
|
"metadata": {"name": "nginx"},
|
||||||
|
"spec": {
|
||||||
|
"accessModes": ["ReadWriteOnce"],
|
||||||
|
"resources": {"requests": {"storage": "1Gi"}}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
obj, err := client.AppsV1().RESTClient().Patch(types.ApplyPatchType).
|
||||||
|
Namespace("default").
|
||||||
|
Param("fieldManager", "apply_test").
|
||||||
|
Resource("statefulsets").
|
||||||
|
Name("nginx").
|
||||||
|
Body(ssBytes).
|
||||||
|
Do(context.TODO()).
|
||||||
|
Get()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create object: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rvCreated, err := getRV(obj)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get RV: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sleep for one second to make sure that the times of each update operation is different.
|
||||||
|
time.Sleep(1200 * time.Millisecond)
|
||||||
|
|
||||||
|
obj, err = client.AppsV1().RESTClient().Patch(types.ApplyPatchType).
|
||||||
|
Namespace("default").
|
||||||
|
Param("fieldManager", "apply_test").
|
||||||
|
Resource("statefulsets").
|
||||||
|
Name("nginx").
|
||||||
|
Body(ssBytes).
|
||||||
|
Do(context.TODO()).
|
||||||
|
Get()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create object: %v", err)
|
||||||
|
}
|
||||||
|
rvApplied, err := getRV(obj)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get RV: %v", err)
|
||||||
|
}
|
||||||
|
if rvApplied != rvCreated {
|
||||||
|
t.Fatal("ResourceVersion changed after apply")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNoSemanticUpdateAppleSameResourceVersion makes sure that PUT requests which makes no semantic changes
|
||||||
|
// will not change the resource version (no write to etcd is done)
|
||||||
|
//
|
||||||
|
// Some of the non-semantic changes are:
|
||||||
|
// - Applying an atomic struct that removes a default
|
||||||
|
// - Changing Quantity or other fields that are normalized
|
||||||
|
func TestNoSemanticUpdatePutSameResourceVersion(t *testing.T) {
|
||||||
|
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)()
|
||||||
|
|
||||||
|
client, closeFn := setup(t)
|
||||||
|
defer closeFn()
|
||||||
|
|
||||||
|
ssBytes := []byte(`{
|
||||||
|
"apiVersion": "apps/v1",
|
||||||
|
"kind": "StatefulSet",
|
||||||
|
"metadata": {
|
||||||
|
"name": "nginx",
|
||||||
|
"labels": {"app": "nginx"}
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"serviceName": "nginx",
|
||||||
|
"selector": { "matchLabels": {"app": "nginx"}},
|
||||||
|
"template": {
|
||||||
|
"metadata": {
|
||||||
|
"labels": {"app": "nginx"}
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"containers": [{
|
||||||
|
"name": "nginx",
|
||||||
|
"image": "nginx",
|
||||||
|
"resources": {
|
||||||
|
"limits": {"memory": "2048Mi"}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"volumeClaimTemplates": [{
|
||||||
|
"metadata": {"name": "nginx"},
|
||||||
|
"spec": {
|
||||||
|
"accessModes": ["ReadWriteOnce"],
|
||||||
|
"resources": { "requests": { "storage": "1Gi"}}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
obj, err := client.AppsV1().RESTClient().Post().
|
||||||
|
Namespace("default").
|
||||||
|
Param("fieldManager", "apply_test").
|
||||||
|
Resource("statefulsets").
|
||||||
|
Body(ssBytes).
|
||||||
|
Do(context.TODO()).
|
||||||
|
Get()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create object: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rvCreated, err := getRV(obj)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get RV: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sleep for one second to make sure that the times of each update operation is different.
|
||||||
|
time.Sleep(1200 * time.Millisecond)
|
||||||
|
|
||||||
|
obj, err = client.AppsV1().RESTClient().Put().
|
||||||
|
Namespace("default").
|
||||||
|
Param("fieldManager", "apply_test").
|
||||||
|
Resource("statefulsets").
|
||||||
|
Name("nginx").
|
||||||
|
Body(ssBytes).
|
||||||
|
Do(context.TODO()).
|
||||||
|
Get()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create object: %v", err)
|
||||||
|
}
|
||||||
|
rvApplied, err := getRV(obj)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get RV: %v", err)
|
||||||
|
}
|
||||||
|
if rvApplied != rvCreated {
|
||||||
|
t.Fatal("ResourceVersion changed after similar PUT")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestCreateOnApplyFailsWithUID makes sure that PATCH requests with the apply content type
|
// TestCreateOnApplyFailsWithUID makes sure that PATCH requests with the apply content type
|
||||||
// will not create the object if it doesn't already exist and it specifies a UID
|
// will not create the object if it doesn't already exist and it specifies a UID
|
||||||
func TestCreateOnApplyFailsWithUID(t *testing.T) {
|
func TestCreateOnApplyFailsWithUID(t *testing.T) {
|
||||||
|
149
test/integration/apiserver/timestamp_transformer_test.go
Normal file
149
test/integration/apiserver/timestamp_transformer_test.go
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package apiserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"math/rand"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
|
k8sfuzz "k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
|
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
|
||||||
|
k8stest "k8s.io/kubernetes/pkg/api/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func convertToUnstructured(b *testing.B, obj runtime.Object) runtime.Object {
|
||||||
|
converter := fieldmanager.DeducedTypeConverter{}
|
||||||
|
typed, err := converter.ObjectToTyped(obj)
|
||||||
|
require.NoError(b, err)
|
||||||
|
res, err := converter.TypedToObject(typed)
|
||||||
|
require.NoError(b, err)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func doBench(b *testing.B, useUnstructured bool, shortCircuit bool) {
|
||||||
|
var (
|
||||||
|
expectedLarge runtime.Object
|
||||||
|
actualLarge runtime.Object
|
||||||
|
expectedSmall runtime.Object
|
||||||
|
actualSmall runtime.Object
|
||||||
|
)
|
||||||
|
|
||||||
|
scheme := runtime.NewScheme()
|
||||||
|
codecs := serializer.NewCodecFactory(scheme)
|
||||||
|
seed := rand.Int63()
|
||||||
|
fuzzer := k8sfuzz.FuzzerFor(k8stest.FuzzerFuncs, rand.NewSource(seed), codecs)
|
||||||
|
fuzzer.NilChance(0)
|
||||||
|
|
||||||
|
fuzzer.MaxDepth(1000).NilChance(0.2).NumElements(2, 15)
|
||||||
|
pod := &v1.Pod{}
|
||||||
|
fuzzer.Fuzz(pod)
|
||||||
|
|
||||||
|
fuzzer.NilChance(0.2).NumElements(10, 100).MaxDepth(10)
|
||||||
|
deployment := &v1.Endpoints{}
|
||||||
|
fuzzer.Fuzz(deployment)
|
||||||
|
|
||||||
|
bts, err := json.Marshal(deployment)
|
||||||
|
require.NoError(b, err)
|
||||||
|
b.Logf("Small (Deployment): %v bytes", len(bts))
|
||||||
|
bts, err = json.Marshal(pod)
|
||||||
|
require.NoError(b, err)
|
||||||
|
b.Logf("Large (Pod): %v bytes", len(bts))
|
||||||
|
|
||||||
|
expectedLarge = deployment
|
||||||
|
expectedSmall = pod
|
||||||
|
|
||||||
|
if useUnstructured {
|
||||||
|
expectedSmall = convertToUnstructured(b, expectedSmall)
|
||||||
|
expectedLarge = convertToUnstructured(b, expectedLarge)
|
||||||
|
}
|
||||||
|
|
||||||
|
actualLarge = expectedLarge.DeepCopyObject()
|
||||||
|
actualSmall = expectedSmall.DeepCopyObject()
|
||||||
|
|
||||||
|
if shortCircuit {
|
||||||
|
// Modify managed fields of the compared objects to induce a short circuit
|
||||||
|
now := metav1.Now()
|
||||||
|
extraEntry := &metav1.ManagedFieldsEntry{
|
||||||
|
Manager: "sidecar_controller",
|
||||||
|
Operation: metav1.ManagedFieldsOperationApply,
|
||||||
|
APIVersion: "apps/v1",
|
||||||
|
Time: &now,
|
||||||
|
FieldsType: "FieldsV1",
|
||||||
|
FieldsV1: &metav1.FieldsV1{
|
||||||
|
Raw: []byte(`{"f:metadata":{"f:labels":{"f:sidecar_version":{}}},"f:spec":{"f:template":{"f:spec":{"f:containers":{"k:{\"name\":\"sidecar\"}":{".":{},"f:image":{},"f:name":{}}}}}}}`),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
largeMeta, err := meta.Accessor(actualLarge)
|
||||||
|
require.NoError(b, err)
|
||||||
|
largeMeta.SetManagedFields(append(largeMeta.GetManagedFields(), *extraEntry))
|
||||||
|
|
||||||
|
smallMeta, err := meta.Accessor(actualSmall)
|
||||||
|
require.NoError(b, err)
|
||||||
|
smallMeta.SetManagedFields(append(smallMeta.GetManagedFields(), *extraEntry))
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
b.Run("Large", func(b2 *testing.B) {
|
||||||
|
for i := 0; i < b2.N; i++ {
|
||||||
|
if _, err := fieldmanager.IgnoreManagedFieldsTimestampsTransformer(
|
||||||
|
context.TODO(),
|
||||||
|
actualLarge,
|
||||||
|
expectedLarge,
|
||||||
|
); err != nil {
|
||||||
|
b2.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("Small", func(b2 *testing.B) {
|
||||||
|
for i := 0; i < b2.N; i++ {
|
||||||
|
if _, err := fieldmanager.IgnoreManagedFieldsTimestampsTransformer(
|
||||||
|
context.TODO(),
|
||||||
|
actualSmall,
|
||||||
|
expectedSmall,
|
||||||
|
); err != nil {
|
||||||
|
b2.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkIgnoreManagedFieldsTimestampTransformerStructuredShortCircuit(b *testing.B) {
|
||||||
|
doBench(b, false, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkIgnoreManagedFieldsTimestampTransformerStructuredWorstCase(b *testing.B) {
|
||||||
|
doBench(b, false, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkIgnoreManagedFieldsTimestampTransformerUnstructuredShortCircuit(b *testing.B) {
|
||||||
|
doBench(b, true, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkIgnoreManagedFieldsTimestampTransformerUnstructuredWorstCase(b *testing.B) {
|
||||||
|
doBench(b, true, false)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user