Skip to content

Commit 8e33abb

Browse files
authored
Merge pull request #237 from apelisse/proper-comparison
Do not use Compare for Equality checks
2 parents ab402c1 + d1b78eb commit 8e33abb

File tree

7 files changed

+120
-29
lines changed

7 files changed

+120
-29
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module sigs.k8s.io/structured-merge-diff/v4
33
require gopkg.in/yaml.v2 v2.2.8
44

55
require (
6+
github.com/google/go-cmp v0.5.9 // indirect
67
github.com/google/gofuzz v1.0.0
78
github.com/json-iterator/go v1.1.12
89
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
22
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
33
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4+
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
5+
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
46
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
57
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
68
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=

internal/fixture/state.go

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ import (
2020
"bytes"
2121
"fmt"
2222

23+
"github.com/google/go-cmp/cmp"
24+
2325
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
2426
"sigs.k8s.io/structured-merge-diff/v4/merge"
2527
"sigs.k8s.io/structured-merge-diff/v4/typed"
26-
"sigs.k8s.io/structured-merge-diff/v4/value"
2728
)
2829

2930
// For the sake of tests, a parser is something that can retrieve a
@@ -168,20 +169,47 @@ func (s *State) Apply(obj typed.YAMLObject, version fieldpath.APIVersion, manage
168169

169170
// CompareLive takes a YAML string and returns the comparison with the
170171
// current live object or an error.
171-
func (s *State) CompareLive(obj typed.YAMLObject, version fieldpath.APIVersion) (*typed.Comparison, error) {
172+
func (s *State) CompareLive(obj typed.YAMLObject, version fieldpath.APIVersion) (string, error) {
172173
obj = FixTabsOrDie(obj)
173174
if err := s.checkInit(version); err != nil {
174-
return nil, err
175+
return "", err
175176
}
176177
tv, err := s.Parser.Type(string(version)).FromYAML(obj)
177178
if err != nil {
178-
return nil, err
179+
return "", err
179180
}
180181
live, err := s.Updater.Converter.Convert(s.Live, version)
181182
if err != nil {
182-
return nil, err
183+
return "", err
184+
}
185+
tvu := convertMapAnyToMapString(tv.AsValue().Unstructured())
186+
liveu := convertMapAnyToMapString(live.AsValue().Unstructured())
187+
return cmp.Diff(tvu, liveu), nil
188+
}
189+
190+
func convertMapAnyToMapString(obj interface{}) interface{} {
191+
switch m := obj.(type) {
192+
case map[string]interface{}:
193+
out := map[string]interface{}{}
194+
for key, value := range m {
195+
out[key] = convertMapAnyToMapString(value)
196+
}
197+
return out
198+
case map[interface{}]interface{}:
199+
out := map[string]interface{}{}
200+
for key, value := range m {
201+
out[key.(string)] = convertMapAnyToMapString(value)
202+
}
203+
return out
204+
case []interface{}:
205+
out := []interface{}{}
206+
for _, value := range m {
207+
out = append(out, convertMapAnyToMapString(value))
208+
}
209+
return out
210+
default:
211+
return obj
183212
}
184-
return live.Compare(tv)
185213
}
186214

187215
// dummyConverter doesn't convert, it just returns the same exact object, as long as a version is provided.
@@ -509,6 +537,9 @@ type TestCase struct {
509537
Managed fieldpath.ManagedFields
510538
// Set to true if the test case needs the union behavior enabled.
511539
RequiresUnions bool
540+
// ReportInputOnNoop if we don't want to compare the output and
541+
// always return it.
542+
ReturnInputOnNoop bool
512543
// IgnoredFields containing the set to ignore for every version
513544
IgnoredFields map[fieldpath.APIVersion]*fieldpath.Set
514545
}
@@ -541,13 +572,16 @@ func (tc TestCase) PreprocessOperations(parser Parser) error {
541572
// TestWithConverter once and reset the benchmark, to make sure the test case
542573
// actually passes..
543574
func (tc TestCase) BenchWithConverter(parser Parser, converter merge.Converter) error {
575+
updaterBuilder := merge.UpdaterBuilder{
576+
Converter: converter,
577+
IgnoredFields: tc.IgnoredFields,
578+
ReturnInputOnNoop: tc.ReturnInputOnNoop,
579+
EnableUnions: tc.RequiresUnions,
580+
}
544581
state := State{
545-
Updater: &merge.Updater{Converter: converter, IgnoredFields: tc.IgnoredFields},
582+
Updater: updaterBuilder.BuildUpdater(),
546583
Parser: parser,
547584
}
548-
if tc.RequiresUnions {
549-
state.Updater.EnableUnionFeature()
550-
}
551585
// We currently don't have any test that converts, we can take
552586
// care of that later.
553587
for i, ops := range tc.Ops {
@@ -561,13 +595,16 @@ func (tc TestCase) BenchWithConverter(parser Parser, converter merge.Converter)
561595

562596
// TestWithConverter runs the test-case using the given parser and converter.
563597
func (tc TestCase) TestWithConverter(parser Parser, converter merge.Converter) error {
598+
updaterBuilder := merge.UpdaterBuilder{
599+
Converter: converter,
600+
IgnoredFields: tc.IgnoredFields,
601+
ReturnInputOnNoop: tc.ReturnInputOnNoop,
602+
EnableUnions: tc.RequiresUnions,
603+
}
564604
state := State{
565-
Updater: &merge.Updater{Converter: converter, IgnoredFields: tc.IgnoredFields},
605+
Updater: updaterBuilder.BuildUpdater(),
566606
Parser: parser,
567607
}
568-
if tc.RequiresUnions {
569-
state.Updater.EnableUnionFeature()
570-
}
571608
for i, ops := range tc.Ops {
572609
err := ops.run(&state)
573610
if err != nil {
@@ -581,8 +618,8 @@ func (tc TestCase) TestWithConverter(parser Parser, converter merge.Converter) e
581618
if err != nil {
582619
return fmt.Errorf("failed to compare live with config: %v", err)
583620
}
584-
if !comparison.IsSame() {
585-
return fmt.Errorf("expected live and config to be the same:\n%v\nConfig: %v\n", comparison, value.ToString(state.Live.AsValue()))
621+
if comparison != "" {
622+
return fmt.Errorf("expected live and config to be the same:\n%v\n", comparison)
586623
}
587624
}
588625

merge/obsolete_versions_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ func TestApplyObsoleteVersion(t *testing.T) {
127127
if err != nil {
128128
t.Fatalf("Failed to compare live object: %v", err)
129129
}
130-
if !comparison.IsSame() {
130+
if comparison != "" {
131131
t.Fatalf("Unexpected object:\n%v", comparison)
132132
}
133133
}

merge/real_test.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,9 @@ func BenchmarkOperations(b *testing.B) {
108108
b.Run(bench.name, func(b *testing.B) {
109109
obj := typed.YAMLObject(read(testdata(bench.filename)))
110110
tests := []struct {
111-
name string
112-
ops []Operation
111+
name string
112+
returnInputonNoop bool
113+
ops []Operation
113114
}{
114115
{
115116
name: "Create",
@@ -146,6 +147,22 @@ func BenchmarkOperations(b *testing.B) {
146147
},
147148
},
148149
},
150+
{
151+
name: "ApplyTwiceNoCompare",
152+
returnInputonNoop: true,
153+
ops: []Operation{
154+
Apply{
155+
Manager: "controller",
156+
APIVersion: "v1",
157+
Object: obj,
158+
},
159+
Apply{
160+
Manager: "other-controller",
161+
APIVersion: "v1",
162+
Object: obj,
163+
},
164+
},
165+
},
149166
{
150167
name: "Update",
151168
ops: []Operation{
@@ -180,7 +197,8 @@ func BenchmarkOperations(b *testing.B) {
180197
for _, test := range tests {
181198
b.Run(test.name, func(b *testing.B) {
182199
tc := TestCase{
183-
Ops: test.ops,
200+
Ops: test.ops,
201+
ReturnInputOnNoop: test.returnInputonNoop,
184202
}
185203
p := SameVersionParser{T: bench.parseType}
186204
tc.PreprocessOperations(p)

merge/set_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,11 @@ func TestUpdateSet(t *testing.T) {
124124
Object: `
125125
list:
126126
- a
127-
- aprime
128127
- b
128+
- aprime
129129
- c
130-
- cprime
131130
- d
131+
- cprime
132132
`,
133133
APIVersion: "v1",
134134
Managed: fieldpath.ManagedFields{
@@ -189,11 +189,11 @@ func TestUpdateSet(t *testing.T) {
189189
Object: `
190190
list:
191191
- a
192-
- aprime
193192
- b
193+
- aprime
194194
- c
195-
- cprime
196195
- d
196+
- cprime
197197
`,
198198
APIVersion: "v1",
199199
Managed: fieldpath.ManagedFields{

merge/update.go

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818

1919
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
2020
"sigs.k8s.io/structured-merge-diff/v4/typed"
21+
"sigs.k8s.io/structured-merge-diff/v4/value"
2122
)
2223

2324
// Converter is an interface to the conversion logic. The converter
@@ -27,17 +28,50 @@ type Converter interface {
2728
IsMissingVersionError(error) bool
2829
}
2930

31+
// UpdateBuilder allows you to create a new Updater by exposing all of
32+
// the options and setting them once.
33+
type UpdaterBuilder struct {
34+
Converter Converter
35+
IgnoredFields map[fieldpath.APIVersion]*fieldpath.Set
36+
37+
EnableUnions bool
38+
39+
// Stop comparing the new object with old object after applying.
40+
// This was initially used to avoid spurious etcd update, but
41+
// since that's vastly inefficient, we've come-up with a better
42+
// way of doing that. Create this flag to stop it.
43+
// Comparing has become more expensive too now that we're not using
44+
// `Compare` but `value.Equals` so this gives an option to avoid it.
45+
ReturnInputOnNoop bool
46+
}
47+
48+
func (u *UpdaterBuilder) BuildUpdater() *Updater {
49+
return &Updater{
50+
Converter: u.Converter,
51+
IgnoredFields: u.IgnoredFields,
52+
enableUnions: u.EnableUnions,
53+
returnInputOnNoop: u.ReturnInputOnNoop,
54+
}
55+
}
56+
3057
// Updater is the object used to compute updated FieldSets and also
3158
// merge the object on Apply.
3259
type Updater struct {
33-
Converter Converter
60+
// Deprecated: This will eventually become private.
61+
Converter Converter
62+
63+
// Deprecated: This will eventually become private.
3464
IgnoredFields map[fieldpath.APIVersion]*fieldpath.Set
3565

3666
enableUnions bool
67+
68+
returnInputOnNoop bool
3769
}
3870

3971
// EnableUnionFeature turns on union handling. It is disabled by default until the
4072
// feature is complete.
73+
//
74+
// Deprecated: Use the builder instead.
4175
func (s *Updater) EnableUnionFeature() {
4276
s.enableUnions = true
4377
}
@@ -157,8 +191,7 @@ func (s *Updater) Update(liveObject, newObject *typed.TypedValue, version fieldp
157191

158192
// Apply should be called when Apply is run, given the current object as
159193
// well as the configuration that is applied. This will merge the object
160-
// and return it. If the object hasn't changed, nil is returned (the
161-
// managers can still have changed though).
194+
// and return it.
162195
func (s *Updater) Apply(liveObject, configObject *typed.TypedValue, version fieldpath.APIVersion, managers fieldpath.ManagedFields, manager string, force bool) (*typed.TypedValue, fieldpath.ManagedFields, error) {
163196
var err error
164197
managers, err = s.reconcileManagedFieldsWithSchemaChanges(liveObject, managers)
@@ -200,11 +233,11 @@ func (s *Updater) Apply(liveObject, configObject *typed.TypedValue, version fiel
200233
if err != nil {
201234
return nil, fieldpath.ManagedFields{}, fmt.Errorf("failed to prune fields: %v", err)
202235
}
203-
managers, compare, err := s.update(liveObject, newObject, version, managers, manager, force)
236+
managers, _, err = s.update(liveObject, newObject, version, managers, manager, force)
204237
if err != nil {
205238
return nil, fieldpath.ManagedFields{}, err
206239
}
207-
if compare.IsSame() {
240+
if !s.returnInputOnNoop && value.EqualsUsing(value.NewFreelistAllocator(), liveObject.AsValue(), newObject.AsValue()) {
208241
newObject = nil
209242
}
210243
return newObject, managers, nil

0 commit comments

Comments
 (0)