github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/plans/objchange/objchange.go (about) 1 package objchange 2 3 import ( 4 "fmt" 5 6 "github.com/zclconf/go-cty/cty" 7 8 "github.com/hashicorp/terraform-plugin-sdk/internal/configs/configschema" 9 ) 10 11 // ProposedNewObject constructs a proposed new object value by combining the 12 // computed attribute values from "prior" with the configured attribute values 13 // from "config". 14 // 15 // Both value must conform to the given schema's implied type, or this function 16 // will panic. 17 // 18 // The prior value must be wholly known, but the config value may be unknown 19 // or have nested unknown values. 20 // 21 // The merging of the two objects includes the attributes of any nested blocks, 22 // which will be correlated in a manner appropriate for their nesting mode. 23 // Note in particular that the correlation for blocks backed by sets is a 24 // heuristic based on matching non-computed attribute values and so it may 25 // produce strange results with more "extreme" cases, such as a nested set 26 // block where _all_ attributes are computed. 27 func ProposedNewObject(schema *configschema.Block, prior, config cty.Value) cty.Value { 28 // If the config and prior are both null, return early here before 29 // populating the prior block. The prevents non-null blocks from appearing 30 // the proposed state value. 31 if config.IsNull() && prior.IsNull() { 32 return prior 33 } 34 35 if prior.IsNull() { 36 // In this case, we will construct a synthetic prior value that is 37 // similar to the result of decoding an empty configuration block, 38 // which simplifies our handling of the top-level attributes/blocks 39 // below by giving us one non-null level of object to pull values from. 40 prior = AllAttributesNull(schema) 41 } 42 return proposedNewObject(schema, prior, config) 43 } 44 45 // PlannedDataResourceObject is similar to ProposedNewObject but tailored for 46 // planning data resources in particular. Specifically, it replaces the values 47 // of any Computed attributes not set in the configuration with an unknown 48 // value, which serves as a placeholder for a value to be filled in by the 49 // provider when the data resource is finally read. 50 // 51 // Data resources are different because the planning of them is handled 52 // entirely within Terraform Core and not subject to customization by the 53 // provider. This function is, in effect, producing an equivalent result to 54 // passing the ProposedNewObject result into a provider's PlanResourceChange 55 // function, assuming a fixed implementation of PlanResourceChange that just 56 // fills in unknown values as needed. 57 func PlannedDataResourceObject(schema *configschema.Block, config cty.Value) cty.Value { 58 // Our trick here is to run the ProposedNewObject logic with an 59 // entirely-unknown prior value. Because of cty's unknown short-circuit 60 // behavior, any operation on prior returns another unknown, and so 61 // unknown values propagate into all of the parts of the resulting value 62 // that would normally be filled in by preserving the prior state. 63 prior := cty.UnknownVal(schema.ImpliedType()) 64 return proposedNewObject(schema, prior, config) 65 } 66 67 func proposedNewObject(schema *configschema.Block, prior, config cty.Value) cty.Value { 68 if config.IsNull() || !config.IsKnown() { 69 // This is a weird situation, but we'll allow it anyway to free 70 // callers from needing to specifically check for these cases. 71 return prior 72 } 73 if (!prior.Type().IsObjectType()) || (!config.Type().IsObjectType()) { 74 panic("ProposedNewObject only supports object-typed values") 75 } 76 77 // From this point onwards, we can assume that both values are non-null 78 // object types, and that the config value itself is known (though it 79 // may contain nested values that are unknown.) 80 81 newAttrs := map[string]cty.Value{} 82 for name, attr := range schema.Attributes { 83 priorV := prior.GetAttr(name) 84 configV := config.GetAttr(name) 85 var newV cty.Value 86 switch { 87 case attr.Computed && attr.Optional: 88 // This is the trickiest scenario: we want to keep the prior value 89 // if the config isn't overriding it. Note that due to some 90 // ambiguity here, setting an optional+computed attribute from 91 // config and then later switching the config to null in a 92 // subsequent change causes the initial config value to be "sticky" 93 // unless the provider specifically overrides it during its own 94 // plan customization step. 95 if configV.IsNull() { 96 newV = priorV 97 } else { 98 newV = configV 99 } 100 case attr.Computed: 101 // configV will always be null in this case, by definition. 102 // priorV may also be null, but that's okay. 103 newV = priorV 104 default: 105 // For non-computed attributes, we always take the config value, 106 // even if it is null. If it's _required_ then null values 107 // should've been caught during an earlier validation step, and 108 // so we don't really care about that here. 109 newV = configV 110 } 111 newAttrs[name] = newV 112 } 113 114 // Merging nested blocks is a little more complex, since we need to 115 // correlate blocks between both objects and then recursively propose 116 // a new object for each. The correlation logic depends on the nesting 117 // mode for each block type. 118 for name, blockType := range schema.BlockTypes { 119 priorV := prior.GetAttr(name) 120 configV := config.GetAttr(name) 121 var newV cty.Value 122 switch blockType.Nesting { 123 124 case configschema.NestingSingle, configschema.NestingGroup: 125 newV = ProposedNewObject(&blockType.Block, priorV, configV) 126 127 case configschema.NestingList: 128 // Nested blocks are correlated by index. 129 configVLen := 0 130 if configV.IsKnown() && !configV.IsNull() { 131 configVLen = configV.LengthInt() 132 } 133 if configVLen > 0 { 134 newVals := make([]cty.Value, 0, configVLen) 135 for it := configV.ElementIterator(); it.Next(); { 136 idx, configEV := it.Element() 137 if priorV.IsKnown() && (priorV.IsNull() || !priorV.HasIndex(idx).True()) { 138 // If there is no corresponding prior element then 139 // we just take the config value as-is. 140 newVals = append(newVals, configEV) 141 continue 142 } 143 priorEV := priorV.Index(idx) 144 145 newEV := ProposedNewObject(&blockType.Block, priorEV, configEV) 146 newVals = append(newVals, newEV) 147 } 148 // Despite the name, a NestingList might also be a tuple, if 149 // its nested schema contains dynamically-typed attributes. 150 if configV.Type().IsTupleType() { 151 newV = cty.TupleVal(newVals) 152 } else { 153 newV = cty.ListVal(newVals) 154 } 155 } else { 156 // Despite the name, a NestingList might also be a tuple, if 157 // its nested schema contains dynamically-typed attributes. 158 if configV.Type().IsTupleType() { 159 newV = cty.EmptyTupleVal 160 } else { 161 newV = cty.ListValEmpty(blockType.ImpliedType()) 162 } 163 } 164 165 case configschema.NestingMap: 166 // Despite the name, a NestingMap may produce either a map or 167 // object value, depending on whether the nested schema contains 168 // dynamically-typed attributes. 169 if configV.Type().IsObjectType() { 170 // Nested blocks are correlated by key. 171 configVLen := 0 172 if configV.IsKnown() && !configV.IsNull() { 173 configVLen = configV.LengthInt() 174 } 175 if configVLen > 0 { 176 newVals := make(map[string]cty.Value, configVLen) 177 atys := configV.Type().AttributeTypes() 178 for name := range atys { 179 configEV := configV.GetAttr(name) 180 if !priorV.IsKnown() || priorV.IsNull() || !priorV.Type().HasAttribute(name) { 181 // If there is no corresponding prior element then 182 // we just take the config value as-is. 183 newVals[name] = configEV 184 continue 185 } 186 priorEV := priorV.GetAttr(name) 187 188 newEV := ProposedNewObject(&blockType.Block, priorEV, configEV) 189 newVals[name] = newEV 190 } 191 // Although we call the nesting mode "map", we actually use 192 // object values so that elements might have different types 193 // in case of dynamically-typed attributes. 194 newV = cty.ObjectVal(newVals) 195 } else { 196 newV = cty.EmptyObjectVal 197 } 198 } else { 199 configVLen := 0 200 if configV.IsKnown() && !configV.IsNull() { 201 configVLen = configV.LengthInt() 202 } 203 if configVLen > 0 { 204 newVals := make(map[string]cty.Value, configVLen) 205 for it := configV.ElementIterator(); it.Next(); { 206 idx, configEV := it.Element() 207 k := idx.AsString() 208 if priorV.IsKnown() && (priorV.IsNull() || !priorV.HasIndex(idx).True()) { 209 // If there is no corresponding prior element then 210 // we just take the config value as-is. 211 newVals[k] = configEV 212 continue 213 } 214 priorEV := priorV.Index(idx) 215 216 newEV := ProposedNewObject(&blockType.Block, priorEV, configEV) 217 newVals[k] = newEV 218 } 219 newV = cty.MapVal(newVals) 220 } else { 221 newV = cty.MapValEmpty(blockType.ImpliedType()) 222 } 223 } 224 225 case configschema.NestingSet: 226 if !configV.Type().IsSetType() { 227 panic("configschema.NestingSet value is not a set as expected") 228 } 229 230 // Nested blocks are correlated by comparing the element values 231 // after eliminating all of the computed attributes. In practice, 232 // this means that any config change produces an entirely new 233 // nested object, and we only propagate prior computed values 234 // if the non-computed attribute values are identical. 235 var cmpVals [][2]cty.Value 236 if priorV.IsKnown() && !priorV.IsNull() { 237 cmpVals = setElementCompareValues(&blockType.Block, priorV, false) 238 } 239 configVLen := 0 240 if configV.IsKnown() && !configV.IsNull() { 241 configVLen = configV.LengthInt() 242 } 243 if configVLen > 0 { 244 used := make([]bool, len(cmpVals)) // track used elements in case multiple have the same compare value 245 newVals := make([]cty.Value, 0, configVLen) 246 for it := configV.ElementIterator(); it.Next(); { 247 _, configEV := it.Element() 248 var priorEV cty.Value 249 for i, cmp := range cmpVals { 250 if used[i] { 251 continue 252 } 253 if cmp[1].RawEquals(configEV) { 254 priorEV = cmp[0] 255 used[i] = true // we can't use this value on a future iteration 256 break 257 } 258 } 259 if priorEV == cty.NilVal { 260 priorEV = cty.NullVal(blockType.ImpliedType()) 261 } 262 263 newEV := ProposedNewObject(&blockType.Block, priorEV, configEV) 264 newVals = append(newVals, newEV) 265 } 266 newV = cty.SetVal(newVals) 267 } else { 268 newV = cty.SetValEmpty(blockType.Block.ImpliedType()) 269 } 270 271 default: 272 // Should never happen, since the above cases are comprehensive. 273 panic(fmt.Sprintf("unsupported block nesting mode %s", blockType.Nesting)) 274 } 275 276 newAttrs[name] = newV 277 } 278 279 return cty.ObjectVal(newAttrs) 280 } 281 282 // setElementCompareValues takes a known, non-null value of a cty.Set type and 283 // returns a table -- constructed of two-element arrays -- that maps original 284 // set element values to corresponding values that have all of the computed 285 // values removed, making them suitable for comparison with values obtained 286 // from configuration. The element type of the set must conform to the implied 287 // type of the given schema, or this function will panic. 288 // 289 // In the resulting slice, the zeroth element of each array is the original 290 // value and the one-indexed element is the corresponding "compare value". 291 // 292 // This is intended to help correlate prior elements with configured elements 293 // in ProposedNewObject. The result is a heuristic rather than an exact science, 294 // since e.g. two separate elements may reduce to the same value through this 295 // process. The caller must therefore be ready to deal with duplicates. 296 func setElementCompareValues(schema *configschema.Block, set cty.Value, isConfig bool) [][2]cty.Value { 297 ret := make([][2]cty.Value, 0, set.LengthInt()) 298 for it := set.ElementIterator(); it.Next(); { 299 _, ev := it.Element() 300 ret = append(ret, [2]cty.Value{ev, setElementCompareValue(schema, ev, isConfig)}) 301 } 302 return ret 303 } 304 305 // setElementCompareValue creates a new value that has all of the same 306 // non-computed attribute values as the one given but has all computed 307 // attribute values forced to null. 308 // 309 // If isConfig is true then non-null Optional+Computed attribute values will 310 // be preserved. Otherwise, they will also be set to null. 311 // 312 // The input value must conform to the schema's implied type, and the return 313 // value is guaranteed to conform to it. 314 func setElementCompareValue(schema *configschema.Block, v cty.Value, isConfig bool) cty.Value { 315 if v.IsNull() || !v.IsKnown() { 316 return v 317 } 318 319 attrs := map[string]cty.Value{} 320 for name, attr := range schema.Attributes { 321 switch { 322 case attr.Computed && attr.Optional: 323 if isConfig { 324 attrs[name] = v.GetAttr(name) 325 } else { 326 attrs[name] = cty.NullVal(attr.Type) 327 } 328 case attr.Computed: 329 attrs[name] = cty.NullVal(attr.Type) 330 default: 331 attrs[name] = v.GetAttr(name) 332 } 333 } 334 335 for name, blockType := range schema.BlockTypes { 336 switch blockType.Nesting { 337 338 case configschema.NestingSingle, configschema.NestingGroup: 339 attrs[name] = setElementCompareValue(&blockType.Block, v.GetAttr(name), isConfig) 340 341 case configschema.NestingList, configschema.NestingSet: 342 cv := v.GetAttr(name) 343 if cv.IsNull() || !cv.IsKnown() { 344 attrs[name] = cv 345 continue 346 } 347 if l := cv.LengthInt(); l > 0 { 348 elems := make([]cty.Value, 0, l) 349 for it := cv.ElementIterator(); it.Next(); { 350 _, ev := it.Element() 351 elems = append(elems, setElementCompareValue(&blockType.Block, ev, isConfig)) 352 } 353 if blockType.Nesting == configschema.NestingSet { 354 // SetValEmpty would panic if given elements that are not 355 // all of the same type, but that's guaranteed not to 356 // happen here because our input value was _already_ a 357 // set and we've not changed the types of any elements here. 358 attrs[name] = cty.SetVal(elems) 359 } else { 360 attrs[name] = cty.TupleVal(elems) 361 } 362 } else { 363 if blockType.Nesting == configschema.NestingSet { 364 attrs[name] = cty.SetValEmpty(blockType.Block.ImpliedType()) 365 } else { 366 attrs[name] = cty.EmptyTupleVal 367 } 368 } 369 370 case configschema.NestingMap: 371 cv := v.GetAttr(name) 372 if cv.IsNull() || !cv.IsKnown() { 373 attrs[name] = cv 374 continue 375 } 376 elems := make(map[string]cty.Value) 377 for it := cv.ElementIterator(); it.Next(); { 378 kv, ev := it.Element() 379 elems[kv.AsString()] = setElementCompareValue(&blockType.Block, ev, isConfig) 380 } 381 attrs[name] = cty.ObjectVal(elems) 382 383 default: 384 // Should never happen, since the above cases are comprehensive. 385 panic(fmt.Sprintf("unsupported block nesting mode %s", blockType.Nesting)) 386 } 387 } 388 389 return cty.ObjectVal(attrs) 390 }