@@ -193,13 +193,13 @@ func serverSideDiff(config, live *unstructured.Unstructured, opts ...Option) (*D
193193 return buildDiffResult (predictedLiveBytes , liveBytes ), nil
194194}
195195
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 ) {
203203 plManagedFields := predictedLive .GetManagedFields ()
204204 if len (plManagedFields ) == 0 {
205205 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
220220 return nil , fmt .Errorf ("error converting live state from unstructured to %s: %w" , gvk , err )
221221 }
222222
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 {}
228225
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
231227 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 ))
234230 if err != nil {
235231 return nil , fmt .Errorf ("error building managedFields set: %w" , err )
236232 }
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 )
248236 }
249237 }
250- // At this point, comparison holds all mutations that aren't owned by any
251- // of the existing managers.
252238
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 )
256241 }
257242
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 )
265246 }
266247
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 )
274259 }
275260
276261 plu := typedPredictedLive .AsValue ().Unstructured ()
@@ -281,6 +266,31 @@ func removeWebhookMutation(predictedLive, live *unstructured.Unstructured, gvkPa
281266 return & unstructured.Unstructured {Object : pl }, nil
282267}
283268
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+
284294func jsonStrToUnstructured (jsonString string ) (* unstructured.Unstructured , error ) {
285295 res := make (map [string ]any )
286296 err := json .Unmarshal ([]byte (jsonString ), & res )
0 commit comments