github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/jsonformat/jsondiff/diff.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package jsondiff 5 6 import ( 7 "reflect" 8 9 "github.com/zclconf/go-cty/cty" 10 11 "github.com/terramate-io/tf/command/jsonformat/collections" 12 "github.com/terramate-io/tf/command/jsonformat/computed" 13 "github.com/terramate-io/tf/command/jsonformat/structured" 14 "github.com/terramate-io/tf/plans" 15 ) 16 17 type TransformPrimitiveJson func(before, after interface{}, ctype cty.Type, action plans.Action) computed.Diff 18 type TransformObjectJson func(map[string]computed.Diff, plans.Action) computed.Diff 19 type TransformArrayJson func([]computed.Diff, plans.Action) computed.Diff 20 type TransformUnknownJson func(computed.Diff, plans.Action) computed.Diff 21 type TransformSensitiveJson func(computed.Diff, bool, bool, plans.Action) computed.Diff 22 type TransformTypeChangeJson func(before, after computed.Diff, action plans.Action) computed.Diff 23 24 // JsonOpts defines the external callback functions that callers should 25 // implement to process the supplied diffs. 26 type JsonOpts struct { 27 Primitive TransformPrimitiveJson 28 Object TransformObjectJson 29 Array TransformArrayJson 30 Unknown TransformUnknownJson 31 Sensitive TransformSensitiveJson 32 TypeChange TransformTypeChangeJson 33 } 34 35 // Transform accepts a generic before and after value that is assumed to be JSON 36 // formatted and transforms it into a computed.Diff, using the callbacks 37 // supplied in the JsonOpts class. 38 func (opts JsonOpts) Transform(change structured.Change) computed.Diff { 39 if sensitive, ok := opts.processSensitive(change); ok { 40 return sensitive 41 } 42 43 if unknown, ok := opts.processUnknown(change); ok { 44 return unknown 45 } 46 47 beforeType := GetType(change.Before) 48 afterType := GetType(change.After) 49 50 deleted := afterType == Null && !change.AfterExplicit 51 created := beforeType == Null && !change.BeforeExplicit 52 53 if beforeType == afterType || (created || deleted) { 54 targetType := beforeType 55 if targetType == Null { 56 targetType = afterType 57 } 58 return opts.processUpdate(change, targetType) 59 } 60 61 b := opts.processUpdate(change.AsDelete(), beforeType) 62 a := opts.processUpdate(change.AsCreate(), afterType) 63 return opts.TypeChange(b, a, plans.Update) 64 } 65 66 func (opts JsonOpts) processUpdate(change structured.Change, jtype Type) computed.Diff { 67 switch jtype { 68 case Null: 69 return opts.processPrimitive(change, cty.NilType) 70 case Bool: 71 return opts.processPrimitive(change, cty.Bool) 72 case String: 73 return opts.processPrimitive(change, cty.String) 74 case Number: 75 return opts.processPrimitive(change, cty.Number) 76 case Object: 77 return opts.processObject(change.AsMap()) 78 case Array: 79 return opts.processArray(change.AsSlice()) 80 default: 81 panic("unrecognized json type: " + jtype) 82 } 83 } 84 85 func (opts JsonOpts) processPrimitive(change structured.Change, ctype cty.Type) computed.Diff { 86 beforeMissing := change.Before == nil && !change.BeforeExplicit 87 afterMissing := change.After == nil && !change.AfterExplicit 88 89 var action plans.Action 90 switch { 91 case beforeMissing && !afterMissing: 92 action = plans.Create 93 case !beforeMissing && afterMissing: 94 action = plans.Delete 95 case reflect.DeepEqual(change.Before, change.After): 96 action = plans.NoOp 97 default: 98 action = plans.Update 99 } 100 101 return opts.Primitive(change.Before, change.After, ctype, action) 102 } 103 104 func (opts JsonOpts) processArray(change structured.ChangeSlice) computed.Diff { 105 processIndices := func(beforeIx, afterIx int) computed.Diff { 106 // It's actually really difficult to render the diffs when some indices 107 // within a list are relevant and others aren't. To make this simpler 108 // we just treat all children of a relevant list as also relevant, so we 109 // ignore the relevant attributes field. 110 // 111 // Interestingly the terraform plan builder also agrees with this, and 112 // never sets relevant attributes beneath lists or sets. We're just 113 // going to enforce this logic here as well. If the list is relevant 114 // (decided elsewhere), then every element in the list is also relevant. 115 return opts.Transform(change.GetChild(beforeIx, afterIx)) 116 } 117 118 isObjType := func(value interface{}) bool { 119 return GetType(value) == Object 120 } 121 122 return opts.Array(collections.TransformSlice(change.Before, change.After, processIndices, isObjType)) 123 } 124 125 func (opts JsonOpts) processObject(change structured.ChangeMap) computed.Diff { 126 return opts.Object(collections.TransformMap(change.Before, change.After, change.AllKeys(), func(key string) computed.Diff { 127 child := change.GetChild(key) 128 if !child.RelevantAttributes.MatchesPartial() { 129 child = child.AsNoOp() 130 } 131 132 return opts.Transform(child) 133 })) 134 } 135 136 func (opts JsonOpts) processUnknown(change structured.Change) (computed.Diff, bool) { 137 return change.CheckForUnknown( 138 false, 139 func(current structured.Change) computed.Diff { 140 return opts.Unknown(computed.Diff{}, plans.Create) 141 }, func(current structured.Change, before structured.Change) computed.Diff { 142 return opts.Unknown(opts.Transform(before), plans.Update) 143 }, 144 ) 145 } 146 147 func (opts JsonOpts) processSensitive(change structured.Change) (computed.Diff, bool) { 148 return change.CheckForSensitive(opts.Transform, func(inner computed.Diff, beforeSensitive, afterSensitive bool, action plans.Action) computed.Diff { 149 return opts.Sensitive(inner, beforeSensitive, afterSensitive, action) 150 }) 151 }