1package objchange 2 3import ( 4 "fmt" 5 6 "github.com/zclconf/go-cty/cty" 7 8 "github.com/hashicorp/terraform/internal/configs/configschema" 9) 10 11// ProposedNew 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. 27func ProposedNew(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 // 41 // "All attributes null" happens to be the definition of EmptyValue for 42 // a Block, so we can just delegate to that 43 prior = schema.EmptyValue() 44 } 45 return proposedNew(schema, prior, config) 46} 47 48// PlannedDataResourceObject is similar to proposedNewBlock but tailored for 49// planning data resources in particular. Specifically, it replaces the values 50// of any Computed attributes not set in the configuration with an unknown 51// value, which serves as a placeholder for a value to be filled in by the 52// provider when the data resource is finally read. 53// 54// Data resources are different because the planning of them is handled 55// entirely within Terraform Core and not subject to customization by the 56// provider. This function is, in effect, producing an equivalent result to 57// passing the proposedNewBlock result into a provider's PlanResourceChange 58// function, assuming a fixed implementation of PlanResourceChange that just 59// fills in unknown values as needed. 60func PlannedDataResourceObject(schema *configschema.Block, config cty.Value) cty.Value { 61 // Our trick here is to run the proposedNewBlock logic with an 62 // entirely-unknown prior value. Because of cty's unknown short-circuit 63 // behavior, any operation on prior returns another unknown, and so 64 // unknown values propagate into all of the parts of the resulting value 65 // that would normally be filled in by preserving the prior state. 66 prior := cty.UnknownVal(schema.ImpliedType()) 67 return proposedNew(schema, prior, config) 68} 69 70func proposedNew(schema *configschema.Block, prior, config cty.Value) cty.Value { 71 if config.IsNull() || !config.IsKnown() { 72 // This is a weird situation, but we'll allow it anyway to free 73 // callers from needing to specifically check for these cases. 74 return prior 75 } 76 if (!prior.Type().IsObjectType()) || (!config.Type().IsObjectType()) { 77 panic("ProposedNew only supports object-typed values") 78 } 79 80 // From this point onwards, we can assume that both values are non-null 81 // object types, and that the config value itself is known (though it 82 // may contain nested values that are unknown.) 83 newAttrs := proposedNewAttributes(schema.Attributes, prior, config) 84 85 // Merging nested blocks is a little more complex, since we need to 86 // correlate blocks between both objects and then recursively propose 87 // a new object for each. The correlation logic depends on the nesting 88 // mode for each block type. 89 for name, blockType := range schema.BlockTypes { 90 priorV := prior.GetAttr(name) 91 configV := config.GetAttr(name) 92 newAttrs[name] = proposedNewNestedBlock(blockType, priorV, configV) 93 } 94 95 return cty.ObjectVal(newAttrs) 96} 97 98func proposedNewNestedBlock(schema *configschema.NestedBlock, prior, config cty.Value) cty.Value { 99 // The only time we should encounter an entirely unknown block is from the 100 // use of dynamic with an unknown for_each expression. 101 if !config.IsKnown() { 102 return config 103 } 104 105 var newV cty.Value 106 107 switch schema.Nesting { 108 109 case configschema.NestingSingle, configschema.NestingGroup: 110 newV = ProposedNew(&schema.Block, prior, config) 111 112 case configschema.NestingList: 113 // Nested blocks are correlated by index. 114 configVLen := 0 115 if !config.IsNull() { 116 configVLen = config.LengthInt() 117 } 118 if configVLen > 0 { 119 newVals := make([]cty.Value, 0, configVLen) 120 for it := config.ElementIterator(); it.Next(); { 121 idx, configEV := it.Element() 122 if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) { 123 // If there is no corresponding prior element then 124 // we just take the config value as-is. 125 newVals = append(newVals, configEV) 126 continue 127 } 128 priorEV := prior.Index(idx) 129 130 newEV := ProposedNew(&schema.Block, priorEV, configEV) 131 newVals = append(newVals, newEV) 132 } 133 // Despite the name, a NestingList might also be a tuple, if 134 // its nested schema contains dynamically-typed attributes. 135 if config.Type().IsTupleType() { 136 newV = cty.TupleVal(newVals) 137 } else { 138 newV = cty.ListVal(newVals) 139 } 140 } else { 141 // Despite the name, a NestingList might also be a tuple, if 142 // its nested schema contains dynamically-typed attributes. 143 if config.Type().IsTupleType() { 144 newV = cty.EmptyTupleVal 145 } else { 146 newV = cty.ListValEmpty(schema.ImpliedType()) 147 } 148 } 149 150 case configschema.NestingMap: 151 // Despite the name, a NestingMap may produce either a map or 152 // object value, depending on whether the nested schema contains 153 // dynamically-typed attributes. 154 if config.Type().IsObjectType() { 155 // Nested blocks are correlated by key. 156 configVLen := 0 157 if config.IsKnown() && !config.IsNull() { 158 configVLen = config.LengthInt() 159 } 160 if configVLen > 0 { 161 newVals := make(map[string]cty.Value, configVLen) 162 atys := config.Type().AttributeTypes() 163 for name := range atys { 164 configEV := config.GetAttr(name) 165 if !prior.IsKnown() || prior.IsNull() || !prior.Type().HasAttribute(name) { 166 // If there is no corresponding prior element then 167 // we just take the config value as-is. 168 newVals[name] = configEV 169 continue 170 } 171 priorEV := prior.GetAttr(name) 172 173 newEV := ProposedNew(&schema.Block, priorEV, configEV) 174 newVals[name] = newEV 175 } 176 // Although we call the nesting mode "map", we actually use 177 // object values so that elements might have different types 178 // in case of dynamically-typed attributes. 179 newV = cty.ObjectVal(newVals) 180 } else { 181 newV = cty.EmptyObjectVal 182 } 183 } else { 184 configVLen := 0 185 if config.IsKnown() && !config.IsNull() { 186 configVLen = config.LengthInt() 187 } 188 if configVLen > 0 { 189 newVals := make(map[string]cty.Value, configVLen) 190 for it := config.ElementIterator(); it.Next(); { 191 idx, configEV := it.Element() 192 k := idx.AsString() 193 if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) { 194 // If there is no corresponding prior element then 195 // we just take the config value as-is. 196 newVals[k] = configEV 197 continue 198 } 199 priorEV := prior.Index(idx) 200 201 newEV := ProposedNew(&schema.Block, priorEV, configEV) 202 newVals[k] = newEV 203 } 204 newV = cty.MapVal(newVals) 205 } else { 206 newV = cty.MapValEmpty(schema.ImpliedType()) 207 } 208 } 209 210 case configschema.NestingSet: 211 if !config.Type().IsSetType() { 212 panic("configschema.NestingSet value is not a set as expected") 213 } 214 215 // Nested blocks are correlated by comparing the element values 216 // after eliminating all of the computed attributes. In practice, 217 // this means that any config change produces an entirely new 218 // nested object, and we only propagate prior computed values 219 // if the non-computed attribute values are identical. 220 var cmpVals [][2]cty.Value 221 if prior.IsKnown() && !prior.IsNull() { 222 cmpVals = setElementCompareValues(&schema.Block, prior, false) 223 } 224 configVLen := 0 225 if config.IsKnown() && !config.IsNull() { 226 configVLen = config.LengthInt() 227 } 228 if configVLen > 0 { 229 used := make([]bool, len(cmpVals)) // track used elements in case multiple have the same compare value 230 newVals := make([]cty.Value, 0, configVLen) 231 for it := config.ElementIterator(); it.Next(); { 232 _, configEV := it.Element() 233 var priorEV cty.Value 234 for i, cmp := range cmpVals { 235 if used[i] { 236 continue 237 } 238 if cmp[1].RawEquals(configEV) { 239 priorEV = cmp[0] 240 used[i] = true // we can't use this value on a future iteration 241 break 242 } 243 } 244 if priorEV == cty.NilVal { 245 priorEV = cty.NullVal(schema.ImpliedType()) 246 } 247 248 newEV := ProposedNew(&schema.Block, priorEV, configEV) 249 newVals = append(newVals, newEV) 250 } 251 newV = cty.SetVal(newVals) 252 } else { 253 newV = cty.SetValEmpty(schema.Block.ImpliedType()) 254 } 255 256 default: 257 // Should never happen, since the above cases are comprehensive. 258 panic(fmt.Sprintf("unsupported block nesting mode %s", schema.Nesting)) 259 } 260 return newV 261} 262 263func proposedNewAttributes(attrs map[string]*configschema.Attribute, prior, config cty.Value) map[string]cty.Value { 264 newAttrs := make(map[string]cty.Value, len(attrs)) 265 for name, attr := range attrs { 266 var priorV cty.Value 267 if prior.IsNull() { 268 priorV = cty.NullVal(prior.Type().AttributeType(name)) 269 } else { 270 priorV = prior.GetAttr(name) 271 } 272 273 configV := config.GetAttr(name) 274 var newV cty.Value 275 switch { 276 case attr.Computed && attr.Optional: 277 // This is the trickiest scenario: we want to keep the prior value 278 // if the config isn't overriding it. Note that due to some 279 // ambiguity here, setting an optional+computed attribute from 280 // config and then later switching the config to null in a 281 // subsequent change causes the initial config value to be "sticky" 282 // unless the provider specifically overrides it during its own 283 // plan customization step. 284 if configV.IsNull() { 285 newV = priorV 286 } else { 287 newV = configV 288 } 289 case attr.Computed: 290 // configV will always be null in this case, by definition. 291 // priorV may also be null, but that's okay. 292 newV = priorV 293 default: 294 if attr.NestedType != nil { 295 // For non-computed NestedType attributes, we need to descend 296 // into the individual nested attributes to build the final 297 // value, unless the entire nested attribute is unknown. 298 if !configV.IsKnown() { 299 newV = configV 300 } else { 301 newV = proposedNewNestedType(attr.NestedType, priorV, configV) 302 } 303 } else { 304 // For non-computed attributes, we always take the config value, 305 // even if it is null. If it's _required_ then null values 306 // should've been caught during an earlier validation step, and 307 // so we don't really care about that here. 308 newV = configV 309 } 310 } 311 newAttrs[name] = newV 312 } 313 return newAttrs 314} 315 316func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value) cty.Value { 317 // If the config is null or empty, we will be using this default value. 318 newV := config 319 320 switch schema.Nesting { 321 case configschema.NestingSingle: 322 if !config.IsNull() { 323 newV = cty.ObjectVal(proposedNewAttributes(schema.Attributes, prior, config)) 324 } else { 325 newV = cty.NullVal(config.Type()) 326 } 327 328 case configschema.NestingList: 329 // Nested blocks are correlated by index. 330 configVLen := 0 331 if config.IsKnown() && !config.IsNull() { 332 configVLen = config.LengthInt() 333 } 334 335 if configVLen > 0 { 336 newVals := make([]cty.Value, 0, configVLen) 337 for it := config.ElementIterator(); it.Next(); { 338 idx, configEV := it.Element() 339 if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) { 340 // If there is no corresponding prior element then 341 // we just take the config value as-is. 342 newVals = append(newVals, configEV) 343 continue 344 } 345 priorEV := prior.Index(idx) 346 347 newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV) 348 newVals = append(newVals, cty.ObjectVal(newEV)) 349 } 350 // Despite the name, a NestingList might also be a tuple, if 351 // its nested schema contains dynamically-typed attributes. 352 if config.Type().IsTupleType() { 353 newV = cty.TupleVal(newVals) 354 } else { 355 newV = cty.ListVal(newVals) 356 } 357 } 358 359 case configschema.NestingMap: 360 // Despite the name, a NestingMap may produce either a map or 361 // object value, depending on whether the nested schema contains 362 // dynamically-typed attributes. 363 if config.Type().IsObjectType() { 364 // Nested blocks are correlated by key. 365 configVLen := 0 366 if config.IsKnown() && !config.IsNull() { 367 configVLen = config.LengthInt() 368 } 369 if configVLen > 0 { 370 newVals := make(map[string]cty.Value, configVLen) 371 atys := config.Type().AttributeTypes() 372 for name := range atys { 373 configEV := config.GetAttr(name) 374 if !prior.IsKnown() || prior.IsNull() || !prior.Type().HasAttribute(name) { 375 // If there is no corresponding prior element then 376 // we just take the config value as-is. 377 newVals[name] = configEV 378 continue 379 } 380 priorEV := prior.GetAttr(name) 381 newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV) 382 newVals[name] = cty.ObjectVal(newEV) 383 } 384 // Although we call the nesting mode "map", we actually use 385 // object values so that elements might have different types 386 // in case of dynamically-typed attributes. 387 newV = cty.ObjectVal(newVals) 388 } 389 } else { 390 configVLen := 0 391 if config.IsKnown() && !config.IsNull() { 392 configVLen = config.LengthInt() 393 } 394 if configVLen > 0 { 395 newVals := make(map[string]cty.Value, configVLen) 396 for it := config.ElementIterator(); it.Next(); { 397 idx, configEV := it.Element() 398 k := idx.AsString() 399 if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) { 400 // If there is no corresponding prior element then 401 // we just take the config value as-is. 402 newVals[k] = configEV 403 continue 404 } 405 priorEV := prior.Index(idx) 406 407 newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV) 408 newVals[k] = cty.ObjectVal(newEV) 409 } 410 newV = cty.MapVal(newVals) 411 } 412 } 413 414 case configschema.NestingSet: 415 // Nested blocks are correlated by comparing the element values 416 // after eliminating all of the computed attributes. In practice, 417 // this means that any config change produces an entirely new 418 // nested object, and we only propagate prior computed values 419 // if the non-computed attribute values are identical. 420 var cmpVals [][2]cty.Value 421 if prior.IsKnown() && !prior.IsNull() { 422 cmpVals = setElementCompareValuesFromObject(schema, prior) 423 } 424 configVLen := 0 425 if config.IsKnown() && !config.IsNull() { 426 configVLen = config.LengthInt() 427 } 428 if configVLen > 0 { 429 used := make([]bool, len(cmpVals)) // track used elements in case multiple have the same compare value 430 newVals := make([]cty.Value, 0, configVLen) 431 for it := config.ElementIterator(); it.Next(); { 432 _, configEV := it.Element() 433 var priorEV cty.Value 434 for i, cmp := range cmpVals { 435 if used[i] { 436 continue 437 } 438 if cmp[1].RawEquals(configEV) { 439 priorEV = cmp[0] 440 used[i] = true // we can't use this value on a future iteration 441 break 442 } 443 } 444 if priorEV == cty.NilVal { 445 newVals = append(newVals, configEV) 446 } else { 447 newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV) 448 newVals = append(newVals, cty.ObjectVal(newEV)) 449 } 450 } 451 newV = cty.SetVal(newVals) 452 } 453 } 454 455 return newV 456} 457 458// setElementCompareValues takes a known, non-null value of a cty.Set type and 459// returns a table -- constructed of two-element arrays -- that maps original 460// set element values to corresponding values that have all of the computed 461// values removed, making them suitable for comparison with values obtained 462// from configuration. The element type of the set must conform to the implied 463// type of the given schema, or this function will panic. 464// 465// In the resulting slice, the zeroth element of each array is the original 466// value and the one-indexed element is the corresponding "compare value". 467// 468// This is intended to help correlate prior elements with configured elements 469// in proposedNewBlock. The result is a heuristic rather than an exact science, 470// since e.g. two separate elements may reduce to the same value through this 471// process. The caller must therefore be ready to deal with duplicates. 472func setElementCompareValues(schema *configschema.Block, set cty.Value, isConfig bool) [][2]cty.Value { 473 ret := make([][2]cty.Value, 0, set.LengthInt()) 474 for it := set.ElementIterator(); it.Next(); { 475 _, ev := it.Element() 476 ret = append(ret, [2]cty.Value{ev, setElementCompareValue(schema, ev, isConfig)}) 477 } 478 return ret 479} 480 481// setElementCompareValue creates a new value that has all of the same 482// non-computed attribute values as the one given but has all computed 483// attribute values forced to null. 484// 485// If isConfig is true then non-null Optional+Computed attribute values will 486// be preserved. Otherwise, they will also be set to null. 487// 488// The input value must conform to the schema's implied type, and the return 489// value is guaranteed to conform to it. 490func setElementCompareValue(schema *configschema.Block, v cty.Value, isConfig bool) cty.Value { 491 if v.IsNull() || !v.IsKnown() { 492 return v 493 } 494 495 attrs := map[string]cty.Value{} 496 for name, attr := range schema.Attributes { 497 switch { 498 case attr.Computed && attr.Optional: 499 if isConfig { 500 attrs[name] = v.GetAttr(name) 501 } else { 502 attrs[name] = cty.NullVal(attr.Type) 503 } 504 case attr.Computed: 505 attrs[name] = cty.NullVal(attr.Type) 506 default: 507 attrs[name] = v.GetAttr(name) 508 } 509 } 510 511 for name, blockType := range schema.BlockTypes { 512 elementType := blockType.Block.ImpliedType() 513 514 switch blockType.Nesting { 515 case configschema.NestingSingle, configschema.NestingGroup: 516 attrs[name] = setElementCompareValue(&blockType.Block, v.GetAttr(name), isConfig) 517 518 case configschema.NestingList, configschema.NestingSet: 519 cv := v.GetAttr(name) 520 if cv.IsNull() || !cv.IsKnown() { 521 attrs[name] = cv 522 continue 523 } 524 525 if l := cv.LengthInt(); l > 0 { 526 elems := make([]cty.Value, 0, l) 527 for it := cv.ElementIterator(); it.Next(); { 528 _, ev := it.Element() 529 elems = append(elems, setElementCompareValue(&blockType.Block, ev, isConfig)) 530 } 531 532 switch { 533 case blockType.Nesting == configschema.NestingSet: 534 // SetValEmpty would panic if given elements that are not 535 // all of the same type, but that's guaranteed not to 536 // happen here because our input value was _already_ a 537 // set and we've not changed the types of any elements here. 538 attrs[name] = cty.SetVal(elems) 539 540 // NestingList cases 541 case elementType.HasDynamicTypes(): 542 attrs[name] = cty.TupleVal(elems) 543 default: 544 attrs[name] = cty.ListVal(elems) 545 } 546 } else { 547 switch { 548 case blockType.Nesting == configschema.NestingSet: 549 attrs[name] = cty.SetValEmpty(elementType) 550 551 // NestingList cases 552 case elementType.HasDynamicTypes(): 553 attrs[name] = cty.EmptyTupleVal 554 default: 555 attrs[name] = cty.ListValEmpty(elementType) 556 } 557 } 558 559 case configschema.NestingMap: 560 cv := v.GetAttr(name) 561 if cv.IsNull() || !cv.IsKnown() || cv.LengthInt() == 0 { 562 attrs[name] = cv 563 continue 564 } 565 elems := make(map[string]cty.Value) 566 for it := cv.ElementIterator(); it.Next(); { 567 kv, ev := it.Element() 568 elems[kv.AsString()] = setElementCompareValue(&blockType.Block, ev, isConfig) 569 } 570 571 switch { 572 case elementType.HasDynamicTypes(): 573 attrs[name] = cty.ObjectVal(elems) 574 default: 575 attrs[name] = cty.MapVal(elems) 576 } 577 578 default: 579 // Should never happen, since the above cases are comprehensive. 580 panic(fmt.Sprintf("unsupported block nesting mode %s", blockType.Nesting)) 581 } 582 } 583 584 return cty.ObjectVal(attrs) 585} 586 587// setElementCompareValues takes a known, non-null value of a cty.Set type and 588// returns a table -- constructed of two-element arrays -- that maps original 589// set element values to corresponding values that have all of the computed 590// values removed, making them suitable for comparison with values obtained 591// from configuration. The element type of the set must conform to the implied 592// type of the given schema, or this function will panic. 593// 594// In the resulting slice, the zeroth element of each array is the original 595// value and the one-indexed element is the corresponding "compare value". 596// 597// This is intended to help correlate prior elements with configured elements 598// in proposedNewBlock. The result is a heuristic rather than an exact science, 599// since e.g. two separate elements may reduce to the same value through this 600// process. The caller must therefore be ready to deal with duplicates. 601func setElementCompareValuesFromObject(schema *configschema.Object, set cty.Value) [][2]cty.Value { 602 ret := make([][2]cty.Value, 0, set.LengthInt()) 603 for it := set.ElementIterator(); it.Next(); { 604 _, ev := it.Element() 605 ret = append(ret, [2]cty.Value{ev, setElementCompareValueFromObject(schema, ev)}) 606 } 607 return ret 608} 609 610// setElementCompareValue creates a new value that has all of the same 611// non-computed attribute values as the one given but has all computed 612// attribute values forced to null. 613// 614// The input value must conform to the schema's implied type, and the return 615// value is guaranteed to conform to it. 616func setElementCompareValueFromObject(schema *configschema.Object, v cty.Value) cty.Value { 617 if v.IsNull() || !v.IsKnown() { 618 return v 619 } 620 attrs := map[string]cty.Value{} 621 622 for name, attr := range schema.Attributes { 623 attrV := v.GetAttr(name) 624 switch { 625 case attr.Computed: 626 attrs[name] = cty.NullVal(attr.Type) 627 default: 628 attrs[name] = attrV 629 } 630 } 631 632 return cty.ObjectVal(attrs) 633} 634