Skip to content

Commit 6f19850

Browse files
mclasmeierMoritz Clasmeierporridge
authored
Improve Metadata Comparison (#68)
Most importantly, we not only exclude `resourceVersion` from the equality check, but also `managedFields` -- it unnecessarily prevents conflict resolution in RHACS operator in real-world situations, which causes repeated reconciliation attempts and multiple errors being logged. --------- Co-authored-by: Moritz Clasmeier <mclasmeier@redhat.com> Co-authored-by: Marcin Owsiany <porridge@redhat.com>
1 parent 4c6d41c commit 6f19850

1 file changed

Lines changed: 34 additions & 13 deletions

File tree

pkg/reconciler/internal/updater/updater.go

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ func (u *Updater) Apply(ctx context.Context, baseObj *unstructured.Unstructured)
149149
return resolveErr
150150
}
151151
if !resolved {
152-
return updateErr
152+
return fmt.Errorf("refreshing object not considered safe during status update: %w", updateErr)
153153
}
154154
return fmt.Errorf("status update conflict") // retriable error.
155155
} else if updateErr != nil {
@@ -182,7 +182,7 @@ func (u *Updater) Apply(ctx context.Context, baseObj *unstructured.Unstructured)
182182
return resolveErr
183183
}
184184
if !resolved {
185-
return updateErr
185+
return fmt.Errorf("refreshing object not considered safe during update: %w", updateErr)
186186
}
187187
return fmt.Errorf("update conflict due to externally-managed status conditions") // retriable error.
188188
} else if updateErr != nil {
@@ -197,11 +197,11 @@ func (u *Updater) Apply(ctx context.Context, baseObj *unstructured.Unstructured)
197197

198198
func isSafeForUpdate(logger logr.Logger, inMemory *unstructured.Unstructured, onCluster *unstructured.Unstructured) bool {
199199
// Compare metadata (excluding resourceVersion).
200-
inMemoryMetadata := metadataWithoutResourceVersion(inMemory)
201-
onClusterMetadata := metadataWithoutResourceVersion(onCluster)
200+
inMemoryMetadata := reducedMetadata(inMemory)
201+
onClusterMetadata := reducedMetadata(onCluster)
202202
if !reflect.DeepEqual(inMemoryMetadata, onClusterMetadata) {
203-
// Diff in generation. Nothing we can do about it -> Fail.
204-
logger.V(1).Info("Not refreshing object due to generation mismatch",
203+
// Diff in metadata. Nothing we can do about it -> Fail.
204+
logger.V(1).Info("Not refreshing object due to metadata mismatch",
205205
"namespace", inMemory.GetNamespace(),
206206
"name", inMemory.GetName(),
207207
"gkv", inMemory.GroupVersionKind(),
@@ -221,21 +221,42 @@ func isSafeForUpdate(logger logr.Logger, inMemory *unstructured.Unstructured, on
221221
return true
222222
}
223223

224-
func metadataWithoutResourceVersion(u *unstructured.Unstructured) map[string]interface{} {
224+
// recudedMetadata returns the metadata of the given unstructured object,
225+
// excluding a few fields which should not be taken into account
226+
// for equality checks. The excluded fields are:
227+
//
228+
// - metadata.resourceVersion
229+
// - metadata.managedFields
230+
// - metadata.annotation."kubectl.kubernetes.io/last-applied-configuration"
231+
func reducedMetadata(u *unstructured.Unstructured) map[string]interface{} {
225232
metadata, ok := u.Object["metadata"].(map[string]interface{})
226233
if !ok {
227234
return nil
228235
}
229-
modifiedMetadata := make(map[string]interface{}, len(metadata))
230-
for k, v := range metadata {
231-
if k == "resourceVersion" {
232-
continue
233-
}
234-
modifiedMetadata[k] = v
236+
modifiedMetadata := excludeFromMap(metadata, "resourceVersion", "managedFields")
237+
if annotations, found := modifiedMetadata["annotations"].(map[string]interface{}); found {
238+
modifiedMetadata["annotations"] = excludeFromMap(annotations, "kubectl.kubernetes.io/last-applied-configuration")
235239
}
240+
236241
return modifiedMetadata
237242
}
238243

244+
// excludeFromMap returns a new map that contains all key-value pairs from inputMap
245+
// except those whose keys are in the keys slice.
246+
func excludeFromMap(inputMap map[string]interface{}, keys ...string) map[string]interface{} {
247+
resultMap := make(map[string]interface{}, len(inputMap))
248+
excludeKeys := make(map[string]struct{})
249+
for _, k := range keys {
250+
excludeKeys[k] = struct{}{}
251+
}
252+
for k, v := range inputMap {
253+
if _, found := excludeKeys[k]; !found {
254+
resultMap[k] = v
255+
}
256+
}
257+
return resultMap
258+
}
259+
239260
func (u *Updater) tryRefresh(ctx context.Context, obj *unstructured.Unstructured) (bool, error) {
240261
// Re-fetch object with client.
241262
current := &unstructured.Unstructured{}

0 commit comments

Comments
 (0)