@@ -193,13 +193,13 @@ func serverSideDiff(config, live *unstructured.Unstructured, opts ...Option) (*D
193
193
return buildDiffResult (predictedLiveBytes , liveBytes ), nil
194
194
}
195
195
196
- // removeWebhookMutation will compare the predictedLive with live to identify
197
- // changes done by mutation webhooks. Webhook mutations are identified by finding
198
- // changes in predictedLive fields not associated with any manager in the
199
- // managedFields. All fields under this condition will be reverted with their state
200
- // from live. If the given predictedLive does not have the managedFields, an error
201
- // will be returned.
202
- func removeWebhookMutation (predictedLive , live * unstructured.Unstructured , gvkParser * managedfields.GvkParser , _ string ) (* unstructured.Unstructured , error ) {
196
+ // removeWebhookMutation will compare the predictedLive with live to identify changes done by mutation webhooks.
197
+ // Webhook mutations are removed from predictedLive by removing all fields which are not managed by the given 'manager'.
198
+ // At this step, we will only have the fields that are managed by the given 'manager'.
199
+ // It is then merged with the live state and re-assigned to predictedLive. This means that any
200
+ // fields not managed by the specified manager will be reverted with their state from live, including any webhook mutations.
201
+ // If the given predictedLive does not have the managedFields, an error will be returned.
202
+ func removeWebhookMutation (predictedLive , live * unstructured.Unstructured , gvkParser * managedfields.GvkParser , manager string ) (* unstructured.Unstructured , error ) {
203
203
plManagedFields := predictedLive .GetManagedFields ()
204
204
if len (plManagedFields ) == 0 {
205
205
return nil , fmt .Errorf ("predictedLive for resource %s/%s must have the managedFields" , predictedLive .GetKind (), predictedLive .GetName ())
@@ -220,57 +220,42 @@ func removeWebhookMutation(predictedLive, live *unstructured.Unstructured, gvkPa
220
220
return nil , fmt .Errorf ("error converting live state from unstructured to %s: %w" , gvk , err )
221
221
}
222
222
223
- // Compare the predicted live with the live resource
224
- comparison , err := typedLive .Compare (typedPredictedLive )
225
- if err != nil {
226
- return nil , fmt .Errorf ("error comparing predicted resource to live resource: %w" , err )
227
- }
223
+ // Initialize an empty fieldpath.Set to aggregate managed fields for the specified manager
224
+ managerFieldsSet := & fieldpath.Set {}
228
225
229
- // Loop over all existing managers in predicted live resource to identify
230
- // fields mutated (in predicted live) not owned by any manager.
226
+ // Iterate over all ManagedFields entries in predictedLive
231
227
for _ , mfEntry := range plManagedFields {
232
- mfs := & fieldpath.Set {}
233
- err := mfs .FromJSON (bytes .NewReader (mfEntry .FieldsV1 .Raw ))
228
+ managedFieldsSet := & fieldpath.Set {}
229
+ err := managedFieldsSet .FromJSON (bytes .NewReader (mfEntry .FieldsV1 .Raw ))
234
230
if err != nil {
235
231
return nil , fmt .Errorf ("error building managedFields set: %w" , err )
236
232
}
237
- if comparison .Added != nil && ! comparison .Added .Empty () {
238
- // exclude the added fields owned by this manager from the comparison
239
- comparison .Added = comparison .Added .Difference (mfs )
240
- }
241
- if comparison .Modified != nil && ! comparison .Modified .Empty () {
242
- // exclude the modified fields owned by this manager from the comparison
243
- comparison .Modified = comparison .Modified .Difference (mfs )
244
- }
245
- if comparison .Removed != nil && ! comparison .Removed .Empty () {
246
- // exclude the removed fields owned by this manager from the comparison
247
- comparison .Removed = comparison .Removed .Difference (mfs )
233
+ if mfEntry .Manager == manager {
234
+ // Union the fields with the aggregated set
235
+ managerFieldsSet = managerFieldsSet .Union (managedFieldsSet )
248
236
}
249
237
}
250
- // At this point, comparison holds all mutations that aren't owned by any
251
- // of the existing managers.
252
238
253
- if comparison .Added != nil && ! comparison .Added .Empty () {
254
- // remove added fields that aren't owned by any manager
255
- typedPredictedLive = typedPredictedLive .RemoveItems (comparison .Added )
239
+ if managerFieldsSet .Empty () {
240
+ return nil , fmt .Errorf ("no managed fields found for manager: %s" , manager )
256
241
}
257
242
258
- if comparison .Modified != nil && ! comparison .Modified .Empty () {
259
- liveModValues := typedLive .ExtractItems (comparison .Modified , typed .WithAppendKeyFields ())
260
- // revert modified fields not owned by any manager
261
- typedPredictedLive , err = typedPredictedLive .Merge (liveModValues )
262
- if err != nil {
263
- return nil , fmt .Errorf ("error reverting webhook modified fields in predicted live resource: %w" , err )
264
- }
243
+ predictedLiveFieldSet , err := typedPredictedLive .ToFieldSet ()
244
+ if err != nil {
245
+ return nil , fmt .Errorf ("error converting predicted live state to FieldSet: %w" , err )
265
246
}
266
247
267
- if comparison .Removed != nil && ! comparison .Removed .Empty () {
268
- liveRmValues := typedLive .ExtractItems (comparison .Removed , typed .WithAppendKeyFields ())
269
- // revert removed fields not owned by any manager
270
- typedPredictedLive , err = typedPredictedLive .Merge (liveRmValues )
271
- if err != nil {
272
- return nil , fmt .Errorf ("error reverting webhook removed fields in predicted live resource: %w" , err )
273
- }
248
+ // Remove fields from predicted live that are not managed by the provided manager
249
+ nonArgoFieldsSet := predictedLiveFieldSet .Difference (managerFieldsSet )
250
+
251
+ // In case any of the removed fields cause schema violations, we will keep those fields
252
+ nonArgoFieldsSet = safelyRemoveFieldsSet (typedPredictedLive , nonArgoFieldsSet )
253
+ typedPredictedLive = typedPredictedLive .RemoveItems (nonArgoFieldsSet )
254
+
255
+ // Apply the predicted live state to the live state to get a diff without mutation webhook fields
256
+ typedPredictedLive , err = typedLive .Merge (typedPredictedLive )
257
+ if err != nil {
258
+ return nil , fmt .Errorf ("error applying predicted live to live state: %w" , err )
274
259
}
275
260
276
261
plu := typedPredictedLive .AsValue ().Unstructured ()
@@ -281,6 +266,31 @@ func removeWebhookMutation(predictedLive, live *unstructured.Unstructured, gvkPa
281
266
return & unstructured.Unstructured {Object : pl }, nil
282
267
}
283
268
269
+ // safelyRemoveFieldSet will validate if removing the fieldsToRemove set from predictedLive maintains
270
+ // a valid schema. If removing a field in fieldsToRemove is invalid and breaks the schema, it is not safe
271
+ // to remove and will be skipped from removal from predictedLive.
272
+ func safelyRemoveFieldsSet (predictedLive * typed.TypedValue , fieldsToRemove * fieldpath.Set ) * fieldpath.Set {
273
+ // In some cases, we cannot remove fields due to violation of the predicted live schema. In such cases we validate the removal
274
+ // of each field and only include it if the removal is valid.
275
+ testPredictedLive := predictedLive .RemoveItems (fieldsToRemove )
276
+ err := testPredictedLive .Validate ()
277
+ if err != nil {
278
+ adjustedFieldsToRemove := fieldpath .NewSet ()
279
+ fieldsToRemove .Iterate (func (p fieldpath.Path ) {
280
+ singleFieldSet := fieldpath .NewSet (p )
281
+ testSingleRemoval := predictedLive .RemoveItems (singleFieldSet )
282
+ // Check if removing this single field maintains a valid schema
283
+ if testSingleRemoval .Validate () == nil {
284
+ // If valid, add this field to the adjusted set to remove
285
+ adjustedFieldsToRemove .Insert (p )
286
+ }
287
+ })
288
+ return adjustedFieldsToRemove
289
+ }
290
+ // If no violations, return the original set to remove
291
+ return fieldsToRemove
292
+ }
293
+
284
294
func jsonStrToUnstructured (jsonString string ) (* unstructured.Unstructured , error ) {
285
295
res := make (map [string ]any )
286
296
err := json .Unmarshal ([]byte (jsonString ), & res )
0 commit comments