@@ -19,12 +19,15 @@ package structuredmerge
1919import (
2020 "context"
2121 "encoding/json"
22+ "strings"
2223
2324 jsonpatch "github.com/evanphx/json-patch/v5"
25+ "github.com/google/go-cmp/cmp"
2426 "github.com/pkg/errors"
2527 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2628 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2729 "sigs.k8s.io/controller-runtime/pkg/client"
30+ "sigs.k8s.io/yaml"
2831
2932 clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2"
3033 "sigs.k8s.io/cluster-api/internal/contract"
@@ -47,7 +50,7 @@ type dryRunSSAPatchInput struct {
4750}
4851
4952// dryRunSSAPatch uses server side apply dry run to determine if the operation is going to change the actual object.
50- func dryRunSSAPatch (ctx context.Context , dryRunCtx * dryRunSSAPatchInput ) (bool , bool , [] byte , error ) {
53+ func dryRunSSAPatch (ctx context.Context , dryRunCtx * dryRunSSAPatchInput ) (bool , bool , string , string , error ) {
5154 // Compute a request identifier.
5255 // The identifier is unique for a specific request to ensure we don't have to re-run the request
5356 // once we found out that it would not produce a diff.
@@ -56,13 +59,13 @@ func dryRunSSAPatch(ctx context.Context, dryRunCtx *dryRunSSAPatchInput) (bool,
5659 // This ensures that we re-run the request as soon as either original or modified changes.
5760 requestIdentifier , err := ssa .ComputeRequestIdentifier (dryRunCtx .client .Scheme (), dryRunCtx .originalUnstructured .GetResourceVersion (), dryRunCtx .modifiedUnstructured )
5861 if err != nil {
59- return false , false , nil , err
62+ return false , false , "" , "" , err
6063 }
6164
6265 // Check if we already ran this request before by checking if the cache already contains this identifier.
6366 // Note: We only add an identifier to the cache if the result of the dry run was no diff.
6467 if exists := dryRunCtx .ssaCache .Has (requestIdentifier , dryRunCtx .originalUnstructured .GetKind ()); exists {
65- return false , false , nil , nil
68+ return false , false , "" , "" , nil
6669 }
6770
6871 // For dry run we use the same options as for the intent but with adding metadata.managedFields
@@ -74,17 +77,17 @@ func dryRunSSAPatch(ctx context.Context, dryRunCtx *dryRunSSAPatchInput) (bool,
7477
7578 // Add TopologyDryRunAnnotation to notify validation webhooks to skip immutability checks.
7679 if err := unstructured .SetNestedField (dryRunCtx .originalUnstructured .Object , "" , "metadata" , "annotations" , clusterv1 .TopologyDryRunAnnotation ); err != nil {
77- return false , false , nil , errors .Wrap (err , "failed to add topology dry-run annotation to original object" )
80+ return false , false , "" , "" , errors .Wrap (err , "failed to add topology dry-run annotation to original object" )
7881 }
7982 if err := unstructured .SetNestedField (dryRunCtx .modifiedUnstructured .Object , "" , "metadata" , "annotations" , clusterv1 .TopologyDryRunAnnotation ); err != nil {
80- return false , false , nil , errors .Wrap (err , "failed to add topology dry-run annotation to modified object" )
83+ return false , false , "" , "" , errors .Wrap (err , "failed to add topology dry-run annotation to modified object" )
8184 }
8285
8386 // Do a server-side apply dry-run with modifiedUnstructured to get the updated object.
8487 err = dryRunCtx .client .Apply (ctx , client .ApplyConfigurationFromUnstructured (dryRunCtx .modifiedUnstructured ), client .DryRunAll , client .FieldOwner (TopologyManagerName ), client .ForceOwnership )
8588 if err != nil {
8689 // This catches errors like metadata.uid changes.
87- return false , false , nil , errors .Wrap (err , "server side apply dry-run failed for modified object" )
90+ return false , false , "" , "" , errors .Wrap (err , "server side apply dry-run failed for modified object" )
8891 }
8992
9093 // Do a server-side apply dry-run with originalUnstructured to ensure the latest defaulting is applied.
@@ -109,7 +112,7 @@ func dryRunSSAPatch(ctx context.Context, dryRunCtx *dryRunSSAPatchInput) (bool,
109112 dryRunCtx .originalUnstructured .SetManagedFields (nil )
110113 err = dryRunCtx .client .Apply (ctx , client .ApplyConfigurationFromUnstructured (dryRunCtx .originalUnstructured ), client .DryRunAll , client .FieldOwner (TopologyManagerName ), client .ForceOwnership )
111114 if err != nil {
112- return false , false , nil , errors .Wrap (err , "server side apply dry-run failed for original object" )
115+ return false , false , "" , "" , errors .Wrap (err , "server side apply dry-run failed for original object" )
113116 }
114117 // Restore managed fields.
115118 dryRunCtx .originalUnstructured .SetManagedFields (originalUnstructuredManagedFieldsBeforeSSA )
@@ -124,7 +127,7 @@ func dryRunSSAPatch(ctx context.Context, dryRunCtx *dryRunSSAPatchInput) (bool,
124127 // Please note that if other managers made changes to fields that we care about and thus ownership changed,
125128 // this would affect our managed fields as well and we would still detect it by diffing our managed fields.
126129 if err := cleanupManagedFieldsAndAnnotation (dryRunCtx .modifiedUnstructured ); err != nil {
127- return false , false , nil , errors .Wrap (err , "failed to filter topology dry-run annotation on modified object" )
130+ return false , false , "" , "" , errors .Wrap (err , "failed to filter topology dry-run annotation on modified object" )
128131 }
129132
130133 // Also run the function for the originalUnstructured to remove the managedField
@@ -135,7 +138,7 @@ func dryRunSSAPatch(ctx context.Context, dryRunCtx *dryRunSSAPatchInput) (bool,
135138 // Please note that if other managers made changes to fields that we care about and thus ownership changed,
136139 // this would affect our managed fields as well and we would still detect it by diffing our managed fields.
137140 if err := cleanupManagedFieldsAndAnnotation (dryRunCtx .originalUnstructured ); err != nil {
138- return false , false , nil , errors .Wrap (err , "failed to filter topology dry-run annotation on original object" )
141+ return false , false , "" , "" , errors .Wrap (err , "failed to filter topology dry-run annotation on original object" )
139142 }
140143
141144 // Drop the other fields which are not part of our intent.
@@ -145,28 +148,28 @@ func dryRunSSAPatch(ctx context.Context, dryRunCtx *dryRunSSAPatchInput) (bool,
145148 // Compare the output of dry run to the original object.
146149 originalJSON , err := json .Marshal (dryRunCtx .originalUnstructured )
147150 if err != nil {
148- return false , false , nil , err
151+ return false , false , "" , "" , err
149152 }
150153 modifiedJSON , err := json .Marshal (dryRunCtx .modifiedUnstructured )
151154 if err != nil {
152- return false , false , nil , err
155+ return false , false , "" , "" , err
153156 }
154157
155158 rawDiff , err := jsonpatch .CreateMergePatch (originalJSON , modifiedJSON )
156159 if err != nil {
157- return false , false , nil , err
160+ return false , false , "" , "" , err
158161 }
159162
160163 // Determine if there are changes to the spec and object.
161164 diff := & unstructured.Unstructured {}
162165 if err := json .Unmarshal (rawDiff , & diff .Object ); err != nil {
163- return false , false , nil , err
166+ return false , false , "" , "" , err
164167 }
165168
166169 hasChanges := len (diff .Object ) > 0
167170 _ , hasSpecChanges := diff .Object ["spec" ]
168171
169- var changes [] byte
172+ var patchString , diffString string
170173 if hasChanges {
171174 // Cleanup diff by dropping .metadata.managedFields.
172175 ssa .FilterIntent (& ssa.FilterIntentInput {
@@ -177,10 +180,30 @@ func dryRunSSAPatch(ctx context.Context, dryRunCtx *dryRunSSAPatchInput) (bool,
177180
178181 // changes should be empty (not "{}") if diff.Object is empty
179182 if len (diff .Object ) != 0 {
180- changes , err = json .Marshal (diff .Object )
183+ patchBytes , err : = json .Marshal (diff .Object )
181184 if err != nil {
182- return false , false , nil , errors .Wrapf (err , "failed to marshal diff" )
185+ return false , false , "" , "" , errors .Wrapf (err , "failed to marshal diff" )
183186 }
187+ patchString = string (patchBytes )
188+
189+ originalJSONWithChanges , err := jsonpatch .MergePatch (originalJSON , patchBytes )
190+ if err != nil {
191+ return false , false , "" , "" , errors .Wrapf (err , "failed to apply diff to original object" )
192+ }
193+
194+ originalYAML , err := yaml .JSONToYAML (originalJSON )
195+ if err != nil {
196+ return false , false , "" , "" , errors .Wrapf (err , "failed to apply diff to original object" )
197+ }
198+
199+ originalYAMLWithChanges , err := yaml .JSONToYAML (originalJSONWithChanges )
200+ if err != nil {
201+ return false , false , "" , "" , errors .Wrapf (err , "failed to apply diff to original object" )
202+ }
203+
204+ diffString = cmp .Diff (string (originalYAML ), string (originalYAMLWithChanges ))
205+ diffString = strings .ReplaceAll (diffString , "\u00A0 " , " " )
206+ diffString = strings .ReplaceAll (diffString , "\t " , " " )
184207 }
185208 }
186209
@@ -189,7 +212,7 @@ func dryRunSSAPatch(ctx context.Context, dryRunCtx *dryRunSSAPatchInput) (bool,
189212 dryRunCtx .ssaCache .Add (requestIdentifier )
190213 }
191214
192- return hasChanges , hasSpecChanges , changes , nil
215+ return hasChanges , hasSpecChanges , patchString , diffString , nil
193216}
194217
195218// cleanupManagedFieldsAndAnnotation adjusts the obj to remove the topology.cluster.x-k8s.io/dry-run
0 commit comments