11package cmd
22
33import (
4- "errors"
4+ "bytes"
5+ "encoding/json"
56 "fmt"
7+ "log"
68 "os"
79 "strings"
810
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+
926 "github.com/spf13/cobra"
1027 "k8s.io/helm/pkg/helm"
1128
@@ -42,6 +59,7 @@ type diffCmd struct {
4259 install bool
4360 stripTrailingCR bool
4461 normalizeManifests bool
62+ threeWayMerge bool
4563}
4664
4765func (d * diffCmd ) isAllowUnreleased () bool {
@@ -59,6 +77,9 @@ This can be used visualize what changes a helm upgrade will
5977perform.
6078`
6179
80+ var envSettings = cli .New ()
81+ var yamlSeperator = []byte ("\n ---\n " )
82+
6283func newChartCommand () * cobra.Command {
6384 diff := diffCmd {
6485 namespace : os .Getenv ("HELM_NAMESPACE" ),
@@ -98,6 +119,8 @@ func newChartCommand() *cobra.Command {
98119 f := cmd .Flags ()
99120 var kubeconfig string
100121 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")
101124 f .StringVar (& diff .chartVersion , "version" , "" , "specify the exact chart version to use. If this is not specified, the latest version is used" )
102125 f .StringVar (& diff .chartRepo , "repo" , "" , "specify the chart repository url to locate the requested chart" )
103126 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 {
169192 return fmt .Errorf ("Failed to render chart: %s" , err )
170193 }
171194
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+
172214 currentSpecs := make (map [string ]* manifest.MappingResult )
173215 if ! newInstall && ! d .dryRun {
174216 if ! d .noHooks {
@@ -202,6 +244,112 @@ func (d *diffCmd) runHelm3() error {
202244 return nil
203245}
204246
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+
205353func (d * diffCmd ) run () error {
206354 if d .chartVersion == "" && d .devel {
207355 d .chartVersion = ">0.0.0-0"
@@ -287,3 +435,92 @@ func (d *diffCmd) run() error {
287435
288436 return nil
289437}
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