github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/jsonformat/structured/change.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package structured 5 6 import ( 7 "encoding/json" 8 "reflect" 9 10 "github.com/terramate-io/tf/command/jsonformat/structured/attribute_path" 11 "github.com/terramate-io/tf/command/jsonplan" 12 "github.com/terramate-io/tf/command/jsonstate" 13 viewsjson "github.com/terramate-io/tf/command/views/json" 14 "github.com/terramate-io/tf/plans" 15 ) 16 17 // Change contains the unmarshalled generic interface{} types that are output by 18 // the JSON functions in the various json packages (such as jsonplan and 19 // jsonprovider). 20 // 21 // A Change can be converted into a computed.Diff, ready for rendering, with the 22 // ComputeDiffForAttribute, ComputeDiffForOutput, and ComputeDiffForBlock 23 // functions. 24 // 25 // The Before and After fields are actually go-cty values, but we cannot convert 26 // them directly because of the Terraform Cloud redacted endpoint. The redacted 27 // endpoint turns sensitive values into strings regardless of their types. 28 // Because of this, we cannot just do a direct conversion using the ctyjson 29 // package. We would have to iterate through the schema first, find the 30 // sensitive values and their mapped types, update the types inside the schema 31 // to strings, and then go back and do the overall conversion. This isn't 32 // including any of the more complicated parts around what happens if something 33 // was sensitive before and isn't sensitive after or vice versa. This would mean 34 // the type would need to change between the before and after value. It is in 35 // fact just easier to iterate through the values as generic JSON interfaces. 36 type Change struct { 37 38 // BeforeExplicit matches AfterExplicit except references the Before value. 39 BeforeExplicit bool 40 41 // AfterExplicit refers to whether the After value is explicit or 42 // implicit. It is explicit if it has been specified by the user, and 43 // implicit if it has been set as a consequence of other changes. 44 // 45 // For example, explicitly setting a value to null in a list should result 46 // in After being null and AfterExplicit being true. In comparison, 47 // removing an element from a list should also result in After being null 48 // and AfterExplicit being false. Without the explicit information our 49 // functions would not be able to tell the difference between these two 50 // cases. 51 AfterExplicit bool 52 53 // Before contains the value before the proposed change. 54 // 55 // The type of the value should be informed by the schema and cast 56 // appropriately when needed. 57 Before interface{} 58 59 // After contains the value after the proposed change. 60 // 61 // The type of the value should be informed by the schema and cast 62 // appropriately when needed. 63 After interface{} 64 65 // Unknown describes whether the After value is known or unknown at the time 66 // of the plan. In practice, this means the after value should be rendered 67 // simply as `(known after apply)`. 68 // 69 // The concrete value could be a boolean describing whether the entirety of 70 // the After value is unknown, or it could be a list or a map depending on 71 // the schema describing whether specific elements or attributes within the 72 // value are unknown. 73 Unknown interface{} 74 75 // BeforeSensitive matches Unknown, but references whether the Before value 76 // is sensitive. 77 BeforeSensitive interface{} 78 79 // AfterSensitive matches Unknown, but references whether the After value is 80 // sensitive. 81 AfterSensitive interface{} 82 83 // ReplacePaths contains a set of paths that point to attributes/elements 84 // that are causing the overall resource to be replaced rather than simply 85 // updated. 86 ReplacePaths attribute_path.Matcher 87 88 // RelevantAttributes contains a set of paths that point attributes/elements 89 // that we should display. Any element/attribute not matched by this Matcher 90 // should be skipped. 91 RelevantAttributes attribute_path.Matcher 92 } 93 94 // FromJsonChange unmarshals the raw []byte values in the jsonplan.Change 95 // structs into generic interface{} types that can be reasoned about. 96 func FromJsonChange(change jsonplan.Change, relevantAttributes attribute_path.Matcher) Change { 97 return Change{ 98 Before: unmarshalGeneric(change.Before), 99 After: unmarshalGeneric(change.After), 100 Unknown: unmarshalGeneric(change.AfterUnknown), 101 BeforeSensitive: unmarshalGeneric(change.BeforeSensitive), 102 AfterSensitive: unmarshalGeneric(change.AfterSensitive), 103 ReplacePaths: attribute_path.Parse(change.ReplacePaths, false), 104 RelevantAttributes: relevantAttributes, 105 } 106 } 107 108 // FromJsonResource unmarshals the raw values in the jsonstate.Resource structs 109 // into generic interface{} types that can be reasoned about. 110 func FromJsonResource(resource jsonstate.Resource) Change { 111 return Change{ 112 // We model resource formatting as NoOps. 113 Before: unwrapAttributeValues(resource.AttributeValues), 114 After: unwrapAttributeValues(resource.AttributeValues), 115 116 // We have some sensitive values, but we don't have any unknown values. 117 Unknown: false, 118 BeforeSensitive: unmarshalGeneric(resource.SensitiveValues), 119 AfterSensitive: unmarshalGeneric(resource.SensitiveValues), 120 121 // We don't display replacement data for resources, and all attributes 122 // are relevant. 123 ReplacePaths: attribute_path.Empty(false), 124 RelevantAttributes: attribute_path.AlwaysMatcher(), 125 } 126 } 127 128 // FromJsonOutput unmarshals the raw values in the jsonstate.Output structs into 129 // generic interface{} types that can be reasoned about. 130 func FromJsonOutput(output jsonstate.Output) Change { 131 return Change{ 132 // We model resource formatting as NoOps. 133 Before: unmarshalGeneric(output.Value), 134 After: unmarshalGeneric(output.Value), 135 136 // We have some sensitive values, but we don't have any unknown values. 137 Unknown: false, 138 BeforeSensitive: output.Sensitive, 139 AfterSensitive: output.Sensitive, 140 141 // We don't display replacement data for resources, and all attributes 142 // are relevant. 143 ReplacePaths: attribute_path.Empty(false), 144 RelevantAttributes: attribute_path.AlwaysMatcher(), 145 } 146 } 147 148 // FromJsonViewsOutput unmarshals the raw values in the viewsjson.Output structs into 149 // generic interface{} types that can be reasoned about. 150 func FromJsonViewsOutput(output viewsjson.Output) Change { 151 return Change{ 152 // We model resource formatting as NoOps. 153 Before: unmarshalGeneric(output.Value), 154 After: unmarshalGeneric(output.Value), 155 156 // We have some sensitive values, but we don't have any unknown values. 157 Unknown: false, 158 BeforeSensitive: output.Sensitive, 159 AfterSensitive: output.Sensitive, 160 161 // We don't display replacement data for resources, and all attributes 162 // are relevant. 163 ReplacePaths: attribute_path.Empty(false), 164 RelevantAttributes: attribute_path.AlwaysMatcher(), 165 } 166 } 167 168 // CalculateAction does a very simple analysis to make the best guess at the 169 // action this change describes. For complex types such as objects, maps, lists, 170 // or sets it is likely more efficient to work out the action directly instead 171 // of relying on this function. 172 func (change Change) CalculateAction() plans.Action { 173 if (change.Before == nil && !change.BeforeExplicit) && (change.After != nil || change.AfterExplicit) { 174 return plans.Create 175 } 176 if (change.After == nil && !change.AfterExplicit) && (change.Before != nil || change.BeforeExplicit) { 177 return plans.Delete 178 } 179 180 if reflect.DeepEqual(change.Before, change.After) && change.AfterExplicit == change.BeforeExplicit && change.IsAfterSensitive() == change.IsBeforeSensitive() { 181 return plans.NoOp 182 } 183 184 return plans.Update 185 } 186 187 // GetDefaultActionForIteration is used to guess what the change could be for 188 // complex attributes (collections and objects) and blocks. 189 // 190 // You can't really tell the difference between a NoOp and an Update just by 191 // looking at the attribute itself as you need to inspect the children. 192 // 193 // This function returns a Delete or a Create action if the before or after 194 // values were null, and returns a NoOp for all other cases. It should be used 195 // in conjunction with compareActions to calculate the actual action based on 196 // the actions of the children. 197 func (change Change) GetDefaultActionForIteration() plans.Action { 198 if change.Before == nil && change.After == nil { 199 return plans.NoOp 200 } 201 202 if change.Before == nil { 203 return plans.Create 204 } 205 if change.After == nil { 206 return plans.Delete 207 } 208 return plans.NoOp 209 } 210 211 // AsNoOp returns the current change as if it is a NoOp operation. 212 // 213 // Basically it replaces all the after values with the before values. 214 func (change Change) AsNoOp() Change { 215 return Change{ 216 BeforeExplicit: change.BeforeExplicit, 217 AfterExplicit: change.BeforeExplicit, 218 Before: change.Before, 219 After: change.Before, 220 Unknown: false, 221 BeforeSensitive: change.BeforeSensitive, 222 AfterSensitive: change.BeforeSensitive, 223 ReplacePaths: change.ReplacePaths, 224 RelevantAttributes: change.RelevantAttributes, 225 } 226 } 227 228 // AsDelete returns the current change as if it is a Delete operation. 229 // 230 // Basically it replaces all the after values with nil or false. 231 func (change Change) AsDelete() Change { 232 return Change{ 233 BeforeExplicit: change.BeforeExplicit, 234 AfterExplicit: false, 235 Before: change.Before, 236 After: nil, 237 Unknown: nil, 238 BeforeSensitive: change.BeforeSensitive, 239 AfterSensitive: nil, 240 ReplacePaths: change.ReplacePaths, 241 RelevantAttributes: change.RelevantAttributes, 242 } 243 } 244 245 // AsCreate returns the current change as if it is a Create operation. 246 // 247 // Basically it replaces all the before values with nil or false. 248 func (change Change) AsCreate() Change { 249 return Change{ 250 BeforeExplicit: false, 251 AfterExplicit: change.AfterExplicit, 252 Before: nil, 253 After: change.After, 254 Unknown: change.Unknown, 255 BeforeSensitive: nil, 256 AfterSensitive: change.AfterSensitive, 257 ReplacePaths: change.ReplacePaths, 258 RelevantAttributes: change.RelevantAttributes, 259 } 260 } 261 262 func unmarshalGeneric(raw json.RawMessage) interface{} { 263 if raw == nil { 264 return nil 265 } 266 267 var out interface{} 268 if err := json.Unmarshal(raw, &out); err != nil { 269 panic("unrecognized json type: " + err.Error()) 270 } 271 return out 272 } 273 274 func unwrapAttributeValues(values jsonstate.AttributeValues) map[string]interface{} { 275 out := make(map[string]interface{}) 276 for key, value := range values { 277 out[key] = unmarshalGeneric(value) 278 } 279 return out 280 }