Skip to content

Commit e7dd426

Browse files
Rename and document functions used during merging
These explanations especially try to cover the new `missingKey` return value which has been added a bunch of places. These functions might also benefit from other refactors. In particular, the doc'd returns of `handleKeys` make it clear that there is some overlap of `throwSpecViolation` and `statusMismatch`. Signed-off-by: Justin Kulikauskas <[email protected]>
1 parent 963236a commit e7dd426

File tree

4 files changed

+108
-82
lines changed

4 files changed

+108
-82
lines changed

controllers/configurationpolicy_controller.go

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2821,8 +2821,15 @@ type countedVal struct {
28212821
count int
28222822
}
28232823

2824-
// mergeArrays is a helper function that takes a list from the existing object and merges in all the data that is
2825-
// different in the template.
2824+
// mergeArrays performs a deep merge operation, combining the given lists. It
2825+
// specially considers items with "names", which it will merge from each list.
2826+
// It preserves duplicates that are already in `existingArr`, but does not
2827+
// introduce *new* duplicate items if they are in `desiredArr`. When merging
2828+
// items or nested items which are maps, the `zeroValueEqualsNil` parameter
2829+
// determines how to handle certain "zero value" cases (see `deeplyEquivalent`).
2830+
//
2831+
// It returns the merged list, and indicates whether any of the nested maps were
2832+
// considered equivalent due to "zero values".
28262833
func mergeArrays(
28272834
desiredArr []interface{}, existingArr []interface{}, ctype policyv1.ComplianceType, zeroValueEqualsNil bool,
28282835
) (result []interface{}, missingKey bool) {
@@ -2892,12 +2899,12 @@ func mergeArrays(
28922899
}
28932900

28942901
// use map compare helper function to check equality on lists of maps
2895-
mergedObj, missingKey, _ = compareSpecs(val1, val2, ctype, zeroValueEqualsNil)
2902+
mergedObj, missingKey, _ = mergeMaps(val1, val2, ctype, zeroValueEqualsNil)
28962903
default:
28972904
mergedObj = val1
28982905
}
28992906
// if a match is found, this field is already in the template, so we can skip it in future checks
2900-
equal, missing := equalObjWithSort(mergedObj, val2, zeroValueEqualsNil)
2907+
equal, missing := deeplyEquivalent(mergedObj, val2, zeroValueEqualsNil)
29012908
missingKey = missingKey || missing
29022909

29032910
if sameNamedObjects || equal {
@@ -2925,9 +2932,15 @@ func mergeArrays(
29252932
return desiredArr, missingKey
29262933
}
29272934

2928-
// compareSpecs is a wrapper function that creates a merged map for mustHave
2929-
// and returns the template map for mustonlyhave
2930-
func compareSpecs(
2935+
// mergeMaps performs a deep merge operation, combining data from `oldSpec` and
2936+
// `newSpec`, prioritizing the data in `newSpec`. It specially handles lists
2937+
// (see `mergeArrays`) and whether "zero values" should be considered equivalent
2938+
// if only found in one of the inputs (see `deeplyEquivalent`). The "zero value"
2939+
// handling can be configured with the `zeroValueEqualsNil` parameter.
2940+
//
2941+
// It returns the merged object, and indicates if it introduced data for "zero
2942+
// values" from `newSpec` that were not present in `oldSpec`.
2943+
func mergeMaps(
29312944
newSpec, oldSpec map[string]interface{}, ctype policyv1.ComplianceType, zeroValueEqualsNil bool,
29322945
) (updatedSpec map[string]interface{}, missingKey bool, err error) {
29332946
if ctype.IsMustOnlyHave() {
@@ -2939,8 +2952,15 @@ func compareSpecs(
29392952
return merged.(map[string]interface{}), missing, err
29402953
}
29412954

2942-
// handleSingleKey checks whether a key/value pair in an object template matches with that in the existing
2943-
// resource on the cluster
2955+
// handleSingleKey compares and merges a single field in the given objects. It
2956+
// specially considers lists (allowing for different orderings) and whether some
2957+
// "zero values" only found in one of the inputs causes the inputs to not be
2958+
// considered equivalent (see `deeplyEquivalent`). The `zeroValueEqualsNil`
2959+
// parameter can be used to tweak that situation slightly.
2960+
//
2961+
// It returns whether an update is needed, a merged version of the field, whether
2962+
// the field was skipped entirely, and whether any "zero values" were merged in
2963+
// which were not in the `existingObj`.
29442964
func handleSingleKey(
29452965
key string,
29462966
desiredObj *unstructured.Unstructured,
@@ -2988,7 +3008,7 @@ func handleSingleKey(
29883008
case map[string]interface{}:
29893009
switch existingValue := existingValue.(type) {
29903010
case map[string]interface{}:
2991-
mergedValue, missing, err = compareSpecs(desiredValue, existingValue, complianceType, zeroValueEqualsNil)
3011+
mergedValue, missing, err = mergeMaps(desiredValue, existingValue, complianceType, zeroValueEqualsNil)
29923012
missingKey = missingKey || missing
29933013
case nil:
29943014
mergedValue = desiredValue
@@ -3049,7 +3069,7 @@ func handleSingleKey(
30493069
}
30503070

30513071
// sort objects before checking equality to ensure they're in the same order
3052-
equal, missing := equalObjWithSort(mergedValue, existingValue, zeroValueEqualsNil)
3072+
equal, missing := deeplyEquivalent(mergedValue, existingValue, zeroValueEqualsNil)
30533073
missingKey = missingKey || missing
30543074

30553075
if !equal {
@@ -3382,6 +3402,14 @@ func handleDiff(
33823402
// handleKeys goes through all of the fields in the desired object and checks if the existing object
33833403
// matches. When a field is a map or slice, the value in the existing object will be updated with
33843404
// the result of merging its current value with the desired value.
3405+
//
3406+
// It returns:
3407+
// - throwSpecViolation: true if the status has a discrepancy from what is desired
3408+
// - message: information when an error occurs
3409+
// - updateNeeded: true if the object should be updated on the cluster to be enforced
3410+
// - statusMismatch: true if the status has a discrepancy from what is desired
3411+
// - missingKey: true if some nested fields don't strictly match between the existingObj and
3412+
// desiredObj, but the difference is just a "zero" value or omission.
33853413
func handleKeys(
33863414
desiredObj *unstructured.Unstructured,
33873415
existingObj *unstructured.Unstructured,

controllers/configurationpolicy_controller_test.go

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func TestReconcile(t *testing.T) {
8080
t.Log(res)
8181
}
8282

83-
func TestCompareSpecs(t *testing.T) {
83+
func TestMergeMaps(t *testing.T) {
8484
spec1 := map[string]interface{}{
8585
"containers": map[string]string{
8686
"image": "nginx1.7.9",
@@ -94,7 +94,7 @@ func TestCompareSpecs(t *testing.T) {
9494
},
9595
}
9696

97-
merged, _, err := compareSpecs(spec1, spec2, "mustonlyhave", true)
97+
merged, _, err := mergeMaps(spec1, spec2, "mustonlyhave", true)
9898
if err != nil {
9999
t.Fatalf("compareSpecs: (%v)", err)
100100
}
@@ -126,7 +126,7 @@ func TestCompareSpecs(t *testing.T) {
126126
},
127127
}
128128

129-
merged, _, err = compareSpecs(spec1, spec2, "musthave", true)
129+
merged, _, err = mergeMaps(spec1, spec2, "musthave", true)
130130
if err != nil {
131131
t.Fatalf("compareSpecs: (%v)", err)
132132
}
@@ -293,7 +293,7 @@ func TestMergeArraysMustHave(t *testing.T) {
293293

294294
actualMergedList, _ := mergeArrays(test.desiredList, test.currentList, "musthave", true)
295295
assert.Equal(t, fmt.Sprintf("%+v", test.expectedList), fmt.Sprintf("%+v", actualMergedList))
296-
check, _ := checkListsMatch(test.expectedList, actualMergedList)
296+
check, _ := checkListsAreEquivalent(test.expectedList, actualMergedList)
297297
assert.True(t, check)
298298
})
299299
}
@@ -378,13 +378,13 @@ func TestMergeArraysMustOnlyHave(t *testing.T) {
378378

379379
actualMergedList, _ := mergeArrays(test.desiredList, test.currentList, "mustonlyhave", true)
380380
assert.Equal(t, fmt.Sprintf("%+v", test.expectedList), fmt.Sprintf("%+v", actualMergedList))
381-
check, _ := checkListsMatch(test.expectedList, actualMergedList)
381+
check, _ := checkListsAreEquivalent(test.expectedList, actualMergedList)
382382
assert.True(t, check)
383383
})
384384
}
385385
}
386386

387-
func TestCheckListsMatch(t *testing.T) {
387+
func TestCheckListsAreEquivalent(t *testing.T) {
388388
twoFullItems := []interface{}{
389389
map[string]interface{}{
390390
"a": "apple",
@@ -396,7 +396,7 @@ func TestCheckListsMatch(t *testing.T) {
396396
},
397397
}
398398

399-
check, _ := checkListsMatch(twoFullItems, twoFullItems)
399+
check, _ := checkListsAreEquivalent(twoFullItems, twoFullItems)
400400
assert.True(t, check)
401401

402402
twoFullItemsDifferentOrder := []interface{}{
@@ -410,9 +410,9 @@ func TestCheckListsMatch(t *testing.T) {
410410
},
411411
}
412412

413-
check, _ = checkListsMatch(twoFullItems, twoFullItemsDifferentOrder)
413+
check, _ = checkListsAreEquivalent(twoFullItems, twoFullItemsDifferentOrder)
414414
assert.True(t, check)
415-
check, _ = checkListsMatch(twoFullItemsDifferentOrder, twoFullItems)
415+
check, _ = checkListsAreEquivalent(twoFullItemsDifferentOrder, twoFullItems)
416416
assert.True(t, check)
417417

418418
oneFullItem := []interface{}{
@@ -422,9 +422,9 @@ func TestCheckListsMatch(t *testing.T) {
422422
},
423423
}
424424

425-
check, _ = checkListsMatch(twoFullItems, oneFullItem)
425+
check, _ = checkListsAreEquivalent(twoFullItems, oneFullItem)
426426
assert.False(t, check)
427-
check, _ = checkListsMatch(oneFullItem, twoFullItems)
427+
check, _ = checkListsAreEquivalent(oneFullItem, twoFullItems)
428428
assert.False(t, check)
429429

430430
oneSmallItem := []interface{}{
@@ -433,9 +433,9 @@ func TestCheckListsMatch(t *testing.T) {
433433
},
434434
}
435435

436-
check, _ = checkListsMatch(twoFullItems, oneSmallItem)
436+
check, _ = checkListsAreEquivalent(twoFullItems, oneSmallItem)
437437
assert.False(t, check)
438-
check, _ = checkListsMatch(oneSmallItem, twoFullItems)
438+
check, _ = checkListsAreEquivalent(oneSmallItem, twoFullItems)
439439
assert.False(t, check)
440440

441441
twoSmallItems := []interface{}{
@@ -447,9 +447,9 @@ func TestCheckListsMatch(t *testing.T) {
447447
},
448448
}
449449

450-
check, _ = checkListsMatch(twoFullItems, twoSmallItems)
450+
check, _ = checkListsAreEquivalent(twoFullItems, twoSmallItems)
451451
assert.False(t, check)
452-
check, _ = checkListsMatch(twoSmallItems, twoFullItems)
452+
check, _ = checkListsAreEquivalent(twoSmallItems, twoFullItems)
453453
assert.False(t, check)
454454

455455
oneSmallOneBig := []interface{}{
@@ -462,9 +462,9 @@ func TestCheckListsMatch(t *testing.T) {
462462
},
463463
}
464464

465-
check, _ = checkListsMatch(twoFullItems, oneSmallOneBig)
465+
check, _ = checkListsAreEquivalent(twoFullItems, oneSmallOneBig)
466466
assert.False(t, check)
467-
check, _ = checkListsMatch(oneSmallOneBig, twoFullItems)
467+
check, _ = checkListsAreEquivalent(oneSmallOneBig, twoFullItems)
468468
assert.False(t, check)
469469

470470
oneBigOneSmall := []interface{}{
@@ -477,9 +477,9 @@ func TestCheckListsMatch(t *testing.T) {
477477
},
478478
}
479479

480-
check, _ = checkListsMatch(twoFullItems, oneBigOneSmall)
480+
check, _ = checkListsAreEquivalent(twoFullItems, oneBigOneSmall)
481481
assert.False(t, check)
482-
check, _ = checkListsMatch(oneBigOneSmall, twoFullItems)
482+
check, _ = checkListsAreEquivalent(oneBigOneSmall, twoFullItems)
483483
assert.False(t, check)
484484
}
485485

@@ -508,7 +508,7 @@ func TestCheckListsMatchDiffMapLength(t *testing.T) {
508508
},
509509
}
510510

511-
check, _ := checkListsMatch(existingObject, mergedObject)
511+
check, _ := checkListsAreEquivalent(existingObject, mergedObject)
512512
assert.True(t, check)
513513
}
514514

controllers/configurationpolicy_utils.go

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -146,13 +146,18 @@ func addOrUpdateRelatedObject(
146146
return list
147147
}
148148

149-
// equalObjWithSort is a wrapper function that calls the correct function to check equality depending on what
150-
// type the objects to compare are
151-
func equalObjWithSort(mergedObj interface{}, oldObj interface{}, zeroValueEqualsNil bool) (areEqual, missingKey bool) {
149+
// deeplyEquivalent deeply compares the first two inputs, considering them
150+
// equivalent even if they have lists which are in different orders. When the
151+
// `zeroValueEqualsNil` parameter is true, it will allow nested maps to have
152+
// omitted fields that are "zero values" to be considered equivalent.
153+
//
154+
// It returns whether the first two inputs are equivalent, and if any of the
155+
// nested maps were considered equivalent due to "zero values".
156+
func deeplyEquivalent(mergedObj interface{}, oldObj interface{}, zeroValueEqualsNil bool) (areEqual, missingKey bool) {
152157
switch mergedObj := mergedObj.(type) {
153158
case map[string]interface{}:
154159
if oldObjMap, ok := oldObj.(map[string]interface{}); ok {
155-
return checkFieldsWithSort(mergedObj, oldObjMap, zeroValueEqualsNil)
160+
return checkFieldsAreEquivalent(mergedObj, oldObjMap, zeroValueEqualsNil)
156161
}
157162
// this includes the case where oldObj is nil
158163
return false, false
@@ -162,7 +167,7 @@ func equalObjWithSort(mergedObj interface{}, oldObj interface{}, zeroValueEquals
162167
}
163168

164169
if oldObjList, ok := oldObj.([]interface{}); ok {
165-
return checkListsMatch(mergedObj, oldObjList)
170+
return checkListsAreEquivalent(mergedObj, oldObjList)
166171
}
167172

168173
return false, false
@@ -189,9 +194,15 @@ func equalObjWithSort(mergedObj interface{}, oldObj interface{}, zeroValueEquals
189194
}
190195
}
191196

192-
// checkFieldsWithSort is a check for maps that uses an arbitrary sort to ensure it is
193-
// comparing the right values
194-
func checkFieldsWithSort(
197+
// checkFieldsAreEquivalent deeply compares two maps and determines whether they
198+
// contain equivalent fields. During the comparison, it specially considers fields
199+
// which may be omitted because they are "zero values" and lists which might have
200+
// different orderings. The `zeroValueEqualsNil` parameter may be used to disable
201+
// those "zero value" allowances.
202+
//
203+
// It returns whether the two maps are considered equivalent, and if there are
204+
// "extra" fields in `mergedObj` that are just "zero values".
205+
func checkFieldsAreEquivalent(
195206
mergedObj map[string]interface{}, oldObj map[string]interface{}, zeroValueEqualsNil bool,
196207
) (matches, missingKey bool) {
197208
// needed to compare lists, since merge messes up the order
@@ -212,7 +223,7 @@ func checkFieldsWithSort(
212223
return false, missingKey
213224
}
214225

215-
match, missing := checkFieldsWithSort(mVal, oVal, zeroValueEqualsNil)
226+
match, missing := checkFieldsAreEquivalent(mVal, oVal, zeroValueEqualsNil)
216227
missingKey = missingKey || missing
217228

218229
if !match {
@@ -233,7 +244,7 @@ func checkFieldsWithSort(
233244
return false, missingKey
234245
}
235246

236-
match, miss := checkListsMatch(oVal, mVal)
247+
match, miss := checkListsAreEquivalent(oVal, mVal)
237248
missingKey = missingKey || miss
238249

239250
if !match {
@@ -311,8 +322,14 @@ func sortAndSprint(item interface{}) string {
311322
}
312323
}
313324

314-
// checkListsMatch is a generic list check that uses an arbitrary sort to ensure it is comparing the right values
315-
func checkListsMatch(oldVal []interface{}, mergedVal []interface{}) (matches, missingKey bool) {
325+
// checkListsAreEquivalent deeply compares two lists and determines whether they
326+
// contain the same items, regardless of order. When comparing items which are
327+
// maps, it specially considers fields which may be omitted because they are
328+
// "zero values".
329+
//
330+
// It returns whether the two lists are considered equivalent, and if there are
331+
// "extra" fields in items of the `mergedVal` list which are just "zero values".
332+
func checkListsAreEquivalent(oldVal []interface{}, mergedVal []interface{}) (matches, missingKey bool) {
316333
if (oldVal == nil && mergedVal != nil) || (oldVal != nil && mergedVal == nil) {
317334
return false, false
318335
}
@@ -337,7 +354,7 @@ func checkListsMatch(oldVal []interface{}, mergedVal []interface{}) (matches, mi
337354
case map[string]interface{}:
338355
// if list contains maps, recurse on those maps to check for a match
339356
if mVal, ok := mVal[idx].(map[string]interface{}); ok {
340-
match, miss := checkFieldsWithSort(mVal, oNestedVal, true)
357+
match, miss := checkFieldsAreEquivalent(mVal, oNestedVal, true)
341358
missingKey = missingKey || miss
342359

343360
if !match {

0 commit comments

Comments
 (0)