1
1
package cmd
2
2
3
3
import (
4
- "errors"
4
+ "bytes"
5
+ "encoding/json"
5
6
"fmt"
7
+ "log"
6
8
"os"
7
9
"strings"
8
10
11
+ jsoniterator "github.com/json-iterator/go"
12
+ "helm.sh/helm/v3/pkg/action"
13
+ "helm.sh/helm/v3/pkg/cli"
14
+
15
+ jsonpatch "github.com/evanphx/json-patch"
16
+ "github.com/pkg/errors"
17
+ "helm.sh/helm/v3/pkg/kube"
18
+ apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
19
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
20
+ "k8s.io/apimachinery/pkg/runtime"
21
+ "k8s.io/apimachinery/pkg/types"
22
+ "k8s.io/apimachinery/pkg/util/strategicpatch"
23
+ "k8s.io/cli-runtime/pkg/resource"
24
+ "sigs.k8s.io/yaml"
25
+
9
26
"github.com/spf13/cobra"
10
27
"k8s.io/helm/pkg/helm"
11
28
@@ -42,6 +59,7 @@ type diffCmd struct {
42
59
install bool
43
60
stripTrailingCR bool
44
61
normalizeManifests bool
62
+ threeWayMerge bool
45
63
}
46
64
47
65
func (d * diffCmd ) isAllowUnreleased () bool {
@@ -59,6 +77,9 @@ This can be used visualize what changes a helm upgrade will
59
77
perform.
60
78
`
61
79
80
+ var envSettings = cli .New ()
81
+ var yamlSeperator = []byte ("\n ---\n " )
82
+
62
83
func newChartCommand () * cobra.Command {
63
84
diff := diffCmd {
64
85
namespace : os .Getenv ("HELM_NAMESPACE" ),
@@ -98,6 +119,8 @@ func newChartCommand() *cobra.Command {
98
119
f := cmd .Flags ()
99
120
var kubeconfig string
100
121
f .StringVar (& kubeconfig , "kubeconfig" , "" , "This flag is ignored, to allow passing of this top level flag to helm" )
122
+ f .BoolVar (& diff .threeWayMerge , "three-way-merge" , false , "use three-way-merge to compute patch and generate diff output" )
123
+ // f.StringVar(&diff.kubeContext, "kube-context", "", "name of the kubeconfig context to use")
101
124
f .StringVar (& diff .chartVersion , "version" , "" , "specify the exact chart version to use. If this is not specified, the latest version is used" )
102
125
f .StringVar (& diff .chartRepo , "repo" , "" , "specify the chart repository url to locate the requested chart" )
103
126
f .BoolVar (& diff .detailedExitCode , "detailed-exitcode" , false , "return a non-zero exit code when there are changes" )
@@ -169,6 +192,25 @@ func (d *diffCmd) runHelm3() error {
169
192
return fmt .Errorf ("Failed to render chart: %s" , err )
170
193
}
171
194
195
+ if d .threeWayMerge {
196
+ actionConfig := new (action.Configuration )
197
+ if err := actionConfig .Init (envSettings .RESTClientGetter (), envSettings .Namespace (), os .Getenv ("HELM_DRIVER" ), log .Printf ); err != nil {
198
+ log .Fatalf ("%+v" , err )
199
+ }
200
+ if err := actionConfig .KubeClient .IsReachable (); err != nil {
201
+ return err
202
+ }
203
+ original , err := actionConfig .KubeClient .Build (bytes .NewBuffer (releaseManifest ), false )
204
+ if err != nil {
205
+ return errors .Wrap (err , "unable to build kubernetes objects from original release manifest" )
206
+ }
207
+ target , err := actionConfig .KubeClient .Build (bytes .NewBuffer (installManifest ), false )
208
+ if err != nil {
209
+ return errors .Wrap (err , "unable to build kubernetes objects from new release manifest" )
210
+ }
211
+ releaseManifest , installManifest , err = genManifest (original , target )
212
+ }
213
+
172
214
currentSpecs := make (map [string ]* manifest.MappingResult )
173
215
if ! newInstall && ! d .dryRun {
174
216
if ! d .noHooks {
@@ -202,6 +244,112 @@ func (d *diffCmd) runHelm3() error {
202
244
return nil
203
245
}
204
246
247
+ func genManifest (original , target kube.ResourceList ) ([]byte , []byte , error ) {
248
+ var err error
249
+ releaseManifest , installManifest := make ([]byte , 0 ), make ([]byte , 0 )
250
+
251
+ // to be deleted
252
+ targetResources := make (map [string ]bool )
253
+ for _ , r := range target {
254
+ targetResources [objectKey (r )] = true
255
+ }
256
+ for _ , r := range original {
257
+ if ! targetResources [objectKey (r )] {
258
+ out , _ := yaml .Marshal (r .Object )
259
+ releaseManifest = append (releaseManifest , yamlSeperator ... )
260
+ releaseManifest = append (releaseManifest , out ... )
261
+ }
262
+ }
263
+
264
+ existingResources := make (map [string ]bool )
265
+ for _ , r := range original {
266
+ existingResources [objectKey (r )] = true
267
+ }
268
+
269
+ var toBeCreated kube.ResourceList
270
+ for _ , r := range target {
271
+ if ! existingResources [objectKey (r )] {
272
+ toBeCreated = append (toBeCreated , r )
273
+ }
274
+ }
275
+
276
+ toBeUpdated , err := existingResourceConflict (toBeCreated )
277
+ if err != nil {
278
+ return nil , nil , errors .Wrap (err , "rendered manifests contain a resource that already exists. Unable to continue with update" )
279
+ }
280
+
281
+ _ = toBeUpdated .Visit (func (r * resource.Info , err error ) error {
282
+ if err != nil {
283
+ return err
284
+ }
285
+ original .Append (r )
286
+ return nil
287
+ })
288
+
289
+ err = target .Visit (func (info * resource.Info , err error ) error {
290
+ if err != nil {
291
+ return err
292
+ }
293
+ kind := info .Mapping .GroupVersionKind .Kind
294
+
295
+ // Fetch the current object for the three way merge
296
+ helper := resource .NewHelper (info .Client , info .Mapping )
297
+ currentObj , err := helper .Get (info .Namespace , info .Name , info .Export )
298
+ if err != nil {
299
+ if ! apierrors .IsNotFound (err ) {
300
+ return errors .Wrap (err , "could not get information about the resource" )
301
+ }
302
+ // to be created
303
+ out , _ := yaml .Marshal (info .Object )
304
+ installManifest = append (installManifest , yamlSeperator ... )
305
+ installManifest = append (installManifest , out ... )
306
+ return nil
307
+ }
308
+ // to be updated
309
+ out , _ := jsoniterator .ConfigCompatibleWithStandardLibrary .Marshal (currentObj )
310
+ pruneObj , err := deleteStatusAndManagedFields (out )
311
+ if err != nil {
312
+ return errors .Wrapf (err , "prune current obj %q with kind %s" , info .Name , kind )
313
+ }
314
+ pruneOut , err := yaml .Marshal (pruneObj )
315
+ if err != nil {
316
+ return errors .Wrapf (err , "prune current out %q with kind %s" , info .Name , kind )
317
+ }
318
+ releaseManifest = append (releaseManifest , yamlSeperator ... )
319
+ releaseManifest = append (releaseManifest , pruneOut ... )
320
+
321
+ originalInfo := original .Get (info )
322
+ if originalInfo == nil {
323
+ return fmt .Errorf ("could not find %q" , info .Name )
324
+ }
325
+
326
+ patch , patchType , err := createPatch (originalInfo .Object , currentObj , info )
327
+ if err != nil {
328
+ return err
329
+ }
330
+
331
+ helper .ServerDryRun = true
332
+ targetObj , err := helper .Patch (info .Namespace , info .Name , patchType , patch , nil )
333
+ if err != nil {
334
+ return errors .Wrapf (err , "cannot patch %q with kind %s" , info .Name , kind )
335
+ }
336
+ out , _ = jsoniterator .ConfigCompatibleWithStandardLibrary .Marshal (targetObj )
337
+ pruneObj , err = deleteStatusAndManagedFields (out )
338
+ if err != nil {
339
+ return errors .Wrapf (err , "prune current obj %q with kind %s" , info .Name , kind )
340
+ }
341
+ pruneOut , err = yaml .Marshal (pruneObj )
342
+ if err != nil {
343
+ return errors .Wrapf (err , "prune current out %q with kind %s" , info .Name , kind )
344
+ }
345
+ installManifest = append (installManifest , yamlSeperator ... )
346
+ installManifest = append (installManifest , pruneOut ... )
347
+ return nil
348
+ })
349
+
350
+ return releaseManifest , installManifest , err
351
+ }
352
+
205
353
func (d * diffCmd ) run () error {
206
354
if d .chartVersion == "" && d .devel {
207
355
d .chartVersion = ">0.0.0-0"
@@ -287,3 +435,92 @@ func (d *diffCmd) run() error {
287
435
288
436
return nil
289
437
}
438
+
439
+ func createPatch (originalObj , currentObj runtime.Object , target * resource.Info ) ([]byte , types.PatchType , error ) {
440
+ oldData , err := json .Marshal (originalObj )
441
+ if err != nil {
442
+ return nil , types .StrategicMergePatchType , errors .Wrap (err , "serializing current configuration" )
443
+ }
444
+ newData , err := json .Marshal (target .Object )
445
+ if err != nil {
446
+ return nil , types .StrategicMergePatchType , errors .Wrap (err , "serializing target configuration" )
447
+ }
448
+
449
+ // Even if currentObj is nil (because it was not found), it will marshal just fine
450
+ currentData , err := json .Marshal (currentObj )
451
+ if err != nil {
452
+ return nil , types .StrategicMergePatchType , errors .Wrap (err , "serializing live configuration" )
453
+ }
454
+ // kind := target.Mapping.GroupVersionKind.Kind
455
+ // if kind == "Deployment" {
456
+ // curr, _ := yaml.Marshal(currentObj)
457
+ // fmt.Println(string(curr))
458
+ // }
459
+
460
+ // Get a versioned object
461
+ versionedObject := kube .AsVersioned (target )
462
+
463
+ // Unstructured objects, such as CRDs, may not have an not registered error
464
+ // returned from ConvertToVersion. Anything that's unstructured should
465
+ // use the jsonpatch.CreateMergePatch. Strategic Merge Patch is not supported
466
+ // on objects like CRDs.
467
+ _ , isUnstructured := versionedObject .(runtime.Unstructured )
468
+
469
+ // On newer K8s versions, CRDs aren't unstructured but has this dedicated type
470
+ _ , isCRD := versionedObject .(* apiextv1.CustomResourceDefinition )
471
+
472
+ if isUnstructured || isCRD {
473
+ // fall back to generic JSON merge patch
474
+ patch , err := jsonpatch .CreateMergePatch (oldData , newData )
475
+ return patch , types .MergePatchType , err
476
+ }
477
+
478
+ patchMeta , err := strategicpatch .NewPatchMetaFromStruct (versionedObject )
479
+ if err != nil {
480
+ return nil , types .StrategicMergePatchType , errors .Wrap (err , "unable to create patch metadata from object" )
481
+ }
482
+
483
+ patch , err := strategicpatch .CreateThreeWayMergePatch (oldData , newData , currentData , patchMeta , true )
484
+ return patch , types .StrategicMergePatchType , err
485
+ }
486
+
487
+ func objectKey (r * resource.Info ) string {
488
+ gvk := r .Object .GetObjectKind ().GroupVersionKind ()
489
+ return fmt .Sprintf ("%s/%s/%s/%s" , gvk .GroupVersion ().String (), gvk .Kind , r .Namespace , r .Name )
490
+ }
491
+
492
+ func existingResourceConflict (resources kube.ResourceList ) (kube.ResourceList , error ) {
493
+ var requireUpdate kube.ResourceList
494
+
495
+ err := resources .Visit (func (info * resource.Info , err error ) error {
496
+ if err != nil {
497
+ return err
498
+ }
499
+
500
+ helper := resource .NewHelper (info .Client , info .Mapping )
501
+ _ , err = helper .Get (info .Namespace , info .Name , info .Export )
502
+ if err != nil {
503
+ if apierrors .IsNotFound (err ) {
504
+ return nil
505
+ }
506
+ return errors .Wrap (err , "could not get information about the resource" )
507
+ }
508
+
509
+ requireUpdate .Append (info )
510
+ return nil
511
+ })
512
+
513
+ return requireUpdate , err
514
+ }
515
+
516
+ func deleteStatusAndManagedFields (obj []byte ) (map [string ]interface {}, error ) {
517
+ var objectMap map [string ]interface {}
518
+ err := jsoniterator .Unmarshal (obj , & objectMap )
519
+ if err != nil {
520
+ return nil , errors .Wrap (err , "could not unmarshal byte sequence" )
521
+ }
522
+ delete (objectMap , "status" )
523
+ delete (objectMap ["metadata" ].(map [string ]interface {}), "managedFields" )
524
+
525
+ return objectMap , nil
526
+ }
0 commit comments