@@ -6,6 +6,7 @@ package diff
6
6
7
7
import (
8
8
"bytes"
9
+ "context"
9
10
"encoding/json"
10
11
"errors"
11
12
"fmt"
@@ -83,6 +84,14 @@ func Diff(config, live *unstructured.Unstructured, opts ...Option) (*DiffResult,
83
84
Normalize (live , opts ... )
84
85
}
85
86
87
+ if o .serverSideDiff {
88
+ r , err := ServerSideDiff (config , live , opts ... )
89
+ if err != nil {
90
+ return nil , fmt .Errorf ("error calculating server side diff: %w" , err )
91
+ }
92
+ return r , nil
93
+ }
94
+
86
95
// TODO The two variables bellow are necessary because there is a cyclic
87
96
// dependency with the kube package that blocks the usage of constants
88
97
// from common package. common package needs to be refactored and exclude
@@ -120,6 +129,165 @@ func Diff(config, live *unstructured.Unstructured, opts ...Option) (*DiffResult,
120
129
return TwoWayDiff (config , live )
121
130
}
122
131
132
+ // ServerSideDiff will execute a k8s server-side apply in dry-run mode with the
133
+ // given config. The result will be compared with given live resource to determine
134
+ // diff. If config or live are nil it means resource creation or deletion. In this
135
+ // no call will be made to kube-api and a simple diff will be returned.
136
+ func ServerSideDiff (config , live * unstructured.Unstructured , opts ... Option ) (* DiffResult , error ) {
137
+ if live != nil && config != nil {
138
+ result , err := serverSideDiff (config , live , opts ... )
139
+ if err != nil {
140
+ return nil , fmt .Errorf ("serverSideDiff error: %w" , err )
141
+ }
142
+ return result , nil
143
+ }
144
+ // Currently, during resource creation a shallow diff (non ServerSide apply
145
+ // based) will be returned. The reasons are:
146
+ // - Saves 1 additional call to KubeAPI
147
+ // - Much lighter/faster diff
148
+ // - This is the existing behaviour users are already used to
149
+ // - No direct benefit to the user
150
+ result , err := handleResourceCreateOrDeleteDiff (config , live )
151
+ if err != nil {
152
+ return nil , fmt .Errorf ("error handling resource creation or deletion: %w" , err )
153
+ }
154
+ return result , nil
155
+ }
156
+
157
+ // ServerSideDiff will execute a k8s server-side apply in dry-run mode with the
158
+ // given config. The result will be compared with given live resource to determine
159
+ // diff. Modifications done by mutation webhooks are removed from the diff by default.
160
+ // This behaviour can be customized with Option.WithIgnoreMutationWebhook.
161
+ func serverSideDiff (config , live * unstructured.Unstructured , opts ... Option ) (* DiffResult , error ) {
162
+ o := applyOptions (opts )
163
+ if o .serverSideDryRunner == nil {
164
+ return nil , fmt .Errorf ("serverSideDryRunner is null" )
165
+ }
166
+ predictedLiveStr , err := o .serverSideDryRunner .Run (context .Background (), config , o .manager )
167
+ if err != nil {
168
+ return nil , fmt .Errorf ("error running server side apply in dryrun mode: %w" , err )
169
+ }
170
+ predictedLive , err := jsonStrToUnstructured (predictedLiveStr )
171
+ if err != nil {
172
+ return nil , fmt .Errorf ("error converting json string to unstructured: %w" , err )
173
+ }
174
+
175
+ if o .ignoreMutationWebhook {
176
+ predictedLive , err = removeWebhookMutation (predictedLive , live , o .gvkParser , o .manager )
177
+ if err != nil {
178
+ return nil , fmt .Errorf ("error removing non config mutations: %w" , err )
179
+ }
180
+ }
181
+
182
+ Normalize (predictedLive , opts ... )
183
+ unstructured .RemoveNestedField (predictedLive .Object , "metadata" , "managedFields" )
184
+
185
+ predictedLiveBytes , err := json .Marshal (predictedLive )
186
+ if err != nil {
187
+ return nil , fmt .Errorf ("error marshaling predicted live resource: %w" , err )
188
+ }
189
+
190
+ unstructured .RemoveNestedField (live .Object , "metadata" , "managedFields" )
191
+ liveBytes , err := json .Marshal (live )
192
+ if err != nil {
193
+ return nil , fmt .Errorf ("error marshaling live resource: %w" , err )
194
+ }
195
+ return buildDiffResult (predictedLiveBytes , liveBytes ), nil
196
+ }
197
+
198
+ // removeWebhookMutation will compare the predictedLive with live to identify
199
+ // changes done by mutation webhooks. Webhook mutations are identified by finding
200
+ // changes in predictedLive fields not associated with any manager in the
201
+ // managedFields. All fields under this condition will be reverted with their state
202
+ // from live. If the given predictedLive does not have the managedFields, an error
203
+ // will be returned.
204
+ func removeWebhookMutation (predictedLive , live * unstructured.Unstructured , gvkParser * managedfields.GvkParser , manager string ) (* unstructured.Unstructured , error ) {
205
+ plManagedFields := predictedLive .GetManagedFields ()
206
+ if len (plManagedFields ) == 0 {
207
+ return nil , fmt .Errorf ("predictedLive for resource %s/%s must have the managedFields" , predictedLive .GetKind (), predictedLive .GetName ())
208
+ }
209
+ gvk := predictedLive .GetObjectKind ().GroupVersionKind ()
210
+ pt := gvkParser .Type (gvk )
211
+ typedPredictedLive , err := pt .FromUnstructured (predictedLive .Object )
212
+ if err != nil {
213
+ return nil , fmt .Errorf ("error converting predicted live state from unstructured to %s: %w" , gvk , err )
214
+ }
215
+
216
+ typedLive , err := pt .FromUnstructured (live .Object )
217
+ if err != nil {
218
+ return nil , fmt .Errorf ("error converting live state from unstructured to %s: %w" , gvk , err )
219
+ }
220
+
221
+ // Compare the predicted live with the live resource
222
+ comparison , err := typedLive .Compare (typedPredictedLive )
223
+ if err != nil {
224
+ return nil , fmt .Errorf ("error comparing predicted resource to live resource: %w" , err )
225
+ }
226
+
227
+ // Loop over all existing managers in predicted live resource to identify
228
+ // fields mutated (in predicted live) not owned by any manager.
229
+ for _ , mfEntry := range plManagedFields {
230
+ mfs := & fieldpath.Set {}
231
+ err := mfs .FromJSON (bytes .NewReader (mfEntry .FieldsV1 .Raw ))
232
+ if err != nil {
233
+ return nil , fmt .Errorf ("error building managedFields set: %s" , err )
234
+ }
235
+ if comparison .Added != nil && ! comparison .Added .Empty () {
236
+ // exclude the added fields owned by this manager from the comparison
237
+ comparison .Added = comparison .Added .Difference (mfs )
238
+ }
239
+ if comparison .Modified != nil && ! comparison .Modified .Empty () {
240
+ // exclude the modified fields owned by this manager from the comparison
241
+ comparison .Modified = comparison .Modified .Difference (mfs )
242
+ }
243
+ if comparison .Removed != nil && ! comparison .Removed .Empty () {
244
+ // exclude the removed fields owned by this manager from the comparison
245
+ comparison .Removed = comparison .Removed .Difference (mfs )
246
+ }
247
+ }
248
+ // At this point, comparison holds all mutations that aren't owned by any
249
+ // of the existing managers.
250
+
251
+ if comparison .Added != nil && ! comparison .Added .Empty () {
252
+ // remove added fields that aren't owned by any manager
253
+ typedPredictedLive = typedPredictedLive .RemoveItems (comparison .Added )
254
+ }
255
+
256
+ if comparison .Modified != nil && ! comparison .Modified .Empty () {
257
+ liveModValues := typedLive .ExtractItems (comparison .Modified )
258
+ // revert modified fields not owned by any manager
259
+ typedPredictedLive , err = typedPredictedLive .Merge (liveModValues )
260
+ if err != nil {
261
+ return nil , fmt .Errorf ("error reverting webhook modified fields in predicted live resource: %s" , err )
262
+ }
263
+ }
264
+
265
+ if comparison .Removed != nil && ! comparison .Removed .Empty () {
266
+ liveRmValues := typedLive .ExtractItems (comparison .Removed )
267
+ // revert removed fields not owned by any manager
268
+ typedPredictedLive , err = typedPredictedLive .Merge (liveRmValues )
269
+ if err != nil {
270
+ return nil , fmt .Errorf ("error reverting webhook removed fields in predicted live resource: %s" , err )
271
+ }
272
+ }
273
+
274
+ plu := typedPredictedLive .AsValue ().Unstructured ()
275
+ pl , ok := plu .(map [string ]interface {})
276
+ if ! ok {
277
+ return nil , fmt .Errorf ("error converting live typedValue: expected map got %T" , plu )
278
+ }
279
+ return & unstructured.Unstructured {Object : pl }, nil
280
+ }
281
+
282
+ func jsonStrToUnstructured (jsonString string ) (* unstructured.Unstructured , error ) {
283
+ res := make (map [string ]interface {})
284
+ err := json .Unmarshal ([]byte (jsonString ), & res )
285
+ if err != nil {
286
+ return nil , fmt .Errorf ("unmarshal error: %s" , err )
287
+ }
288
+ return & unstructured.Unstructured {Object : res }, nil
289
+ }
290
+
123
291
// StructuredMergeDiff will calculate the diff using the structured-merge-diff
124
292
// k8s library (https://github.com/kubernetes-sigs/structured-merge-diff).
125
293
func StructuredMergeDiff (config , live * unstructured.Unstructured , gvkParser * managedfields.GvkParser , manager string ) (* DiffResult , error ) {
0 commit comments