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