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