mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-22 19:31: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
|
||||
}
|
||||
|
||||
// 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
|
||||
// comparisons that have already been seen, which allows short circuiting on
|
||||
// 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)
|
||||
|
||||
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
|
||||
// an array's type, which has already been filtered for.
|
||||
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 true
|
||||
case reflect.Slice:
|
||||
if (v1.IsNil() || v1.Len() == 0) != (v2.IsNil() || v2.Len() == 0) {
|
||||
return false
|
||||
}
|
||||
if v1.IsNil() || v1.Len() == 0 {
|
||||
return true
|
||||
if equateNilAndEmpty {
|
||||
if (v1.IsNil() || v1.Len() == 0) != (v2.IsNil() || v2.Len() == 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
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() {
|
||||
return false
|
||||
@ -169,7 +189,7 @@ func (e Equalities) deepValueEqual(v1, v2 reflect.Value, visited map[visit]bool,
|
||||
return true
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -178,22 +198,40 @@ func (e Equalities) deepValueEqual(v1, v2 reflect.Value, visited map[visit]bool,
|
||||
if v1.IsNil() || v2.IsNil() {
|
||||
return v1.IsNil() == v2.IsNil()
|
||||
}
|
||||
return e.deepValueEqual(v1.Elem(), v2.Elem(), visited, depth+1)
|
||||
case reflect.Pointer:
|
||||
return e.deepValueEqual(v1.Elem(), v2.Elem(), visited, depth+1)
|
||||
return e.deepValueEqual(v1.Elem(), v2.Elem(), visited, equateNilAndEmpty, depth+1)
|
||||
case reflect.Ptr:
|
||||
return e.deepValueEqual(v1.Elem(), v2.Elem(), visited, equateNilAndEmpty, depth+1)
|
||||
case reflect.Struct:
|
||||
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 true
|
||||
case reflect.Map:
|
||||
if (v1.IsNil() || v1.Len() == 0) != (v2.IsNil() || v2.Len() == 0) {
|
||||
return false
|
||||
}
|
||||
if v1.IsNil() || v1.Len() == 0 {
|
||||
return true
|
||||
if equateNilAndEmpty {
|
||||
if (v1.IsNil() || v1.Len() == 0) != (v2.IsNil() || v2.Len() == 0) {
|
||||
return false
|
||||
}
|
||||
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() {
|
||||
return false
|
||||
@ -202,7 +240,7 @@ func (e Equalities) deepValueEqual(v1, v2 reflect.Value, visited map[visit]bool,
|
||||
return true
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
// function for these types.
|
||||
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 {
|
||||
return a1 == a2
|
||||
}
|
||||
@ -240,7 +286,7 @@ func (e Equalities) DeepEqual(a1, a2 interface{}) bool {
|
||||
if v1.Type() != v2.Type() {
|
||||
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 {
|
||||
|
@ -16,6 +16,10 @@ func TestEqualities(t *testing.T) {
|
||||
type Baz struct {
|
||||
Y Bar
|
||||
}
|
||||
type Zap struct {
|
||||
A []int
|
||||
B map[string][]int
|
||||
}
|
||||
err := e.AddFuncs(
|
||||
func(a, b int) bool {
|
||||
return a+1 == b
|
||||
@ -32,10 +36,12 @@ func TestEqualities(t *testing.T) {
|
||||
X int
|
||||
}
|
||||
|
||||
table := []struct {
|
||||
type Case struct {
|
||||
a, b interface{}
|
||||
equal bool
|
||||
}{
|
||||
}
|
||||
|
||||
table := []Case{
|
||||
{1, 2, true},
|
||||
{2, 1, 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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
transformers := []rest.TransformFunc{p.applyPatch, p.applyAdmission, dedupOwnerReferencesTransformer}
|
||||
if scope.FieldManager != nil {
|
||||
transformers = append(transformers, fieldmanager.IgnoreManagedFieldsTimestampsTransformer)
|
||||
}
|
||||
|
||||
wasCreated := false
|
||||
p.updatedObjectInfo = rest.DefaultUpdatedObjectInfo(nil, p.applyPatch, p.applyAdmission, dedupOwnerReferencesTransformer)
|
||||
p.updatedObjectInfo = rest.DefaultUpdatedObjectInfo(nil, transformers...)
|
||||
requestFunc := func() (runtime.Object, error) {
|
||||
// Pass in UpdateOptions to override UpdateStrategy.AllowUpdateOnCreate
|
||||
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{
|
||||
User: userInfo,
|
||||
ResourceRequest: true,
|
||||
|
@ -238,6 +238,18 @@ var (
|
||||
[]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{
|
||||
deprecatedRequestGauge,
|
||||
requestCounter,
|
||||
@ -256,6 +268,7 @@ var (
|
||||
requestFilterDuration,
|
||||
requestAbortsTotal,
|
||||
requestPostTimeoutTotal,
|
||||
requestTimestampComparisonDuration,
|
||||
}
|
||||
|
||||
// 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())
|
||||
}
|
||||
|
||||
func RecordTimestampComparisonLatency(codePath string, elapsed time.Duration) {
|
||||
requestTimestampComparisonDuration.WithLabelValues(codePath).Observe(elapsed.Seconds())
|
||||
}
|
||||
|
||||
func RecordRequestPostTimeout(source string, status string) {
|
||||
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
|
||||
// will not create the object if it doesn't already exist and it specifies a UID
|
||||
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