github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/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/iaas-resource-provision/iaas-rpc/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. 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 var newV cty.Value 312 switch schema.Nesting { 313 case configschema.NestingSingle: 314 if !config.IsNull() { 315 newV = cty.ObjectVal(proposedNewAttributes(schema.Attributes, prior, config)) 316 } else { 317 newV = cty.NullVal(config.Type()) 318 } 319 320 case configschema.NestingList: 321 // Nested blocks are correlated by index. 322 configVLen := 0 323 if config.IsKnown() && !config.IsNull() { 324 configVLen = config.LengthInt() 325 } 326 if configVLen > 0 { 327 newVals := make([]cty.Value, 0, configVLen) 328 for it := config.ElementIterator(); it.Next(); { 329 idx, configEV := it.Element() 330 if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) { 331 // If there is no corresponding prior element then 332 // we just take the config value as-is. 333 newVals = append(newVals, configEV) 334 continue 335 } 336 priorEV := prior.Index(idx) 337 338 newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV) 339 newVals = append(newVals, cty.ObjectVal(newEV)) 340 } 341 // Despite the name, a NestingList might also be a tuple, if 342 // its nested schema contains dynamically-typed attributes. 343 if config.Type().IsTupleType() { 344 newV = cty.TupleVal(newVals) 345 } else { 346 newV = cty.ListVal(newVals) 347 } 348 } else { 349 newV = cty.NullVal(schema.ImpliedType()) 350 } 351 352 case configschema.NestingMap: 353 // Despite the name, a NestingMap may produce either a map or 354 // object value, depending on whether the nested schema contains 355 // dynamically-typed attributes. 356 if config.Type().IsObjectType() { 357 // Nested blocks are correlated by key. 358 configVLen := 0 359 if config.IsKnown() && !config.IsNull() { 360 configVLen = config.LengthInt() 361 } 362 if configVLen > 0 { 363 newVals := make(map[string]cty.Value, configVLen) 364 atys := config.Type().AttributeTypes() 365 for name := range atys { 366 configEV := config.GetAttr(name) 367 if !prior.IsKnown() || prior.IsNull() || !prior.Type().HasAttribute(name) { 368 // If there is no corresponding prior element then 369 // we just take the config value as-is. 370 newVals[name] = configEV 371 continue 372 } 373 priorEV := prior.GetAttr(name) 374 newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV) 375 newVals[name] = cty.ObjectVal(newEV) 376 } 377 // Although we call the nesting mode "map", we actually use 378 // object values so that elements might have different types 379 // in case of dynamically-typed attributes. 380 newV = cty.ObjectVal(newVals) 381 } else { 382 newV = cty.NullVal(schema.ImpliedType()) 383 } 384 } else { 385 configVLen := 0 386 if config.IsKnown() && !config.IsNull() { 387 configVLen = config.LengthInt() 388 } 389 if configVLen > 0 { 390 newVals := make(map[string]cty.Value, configVLen) 391 for it := config.ElementIterator(); it.Next(); { 392 idx, configEV := it.Element() 393 k := idx.AsString() 394 if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) { 395 // If there is no corresponding prior element then 396 // we just take the config value as-is. 397 newVals[k] = configEV 398 continue 399 } 400 priorEV := prior.Index(idx) 401 402 newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV) 403 newVals[k] = cty.ObjectVal(newEV) 404 } 405 newV = cty.MapVal(newVals) 406 } else { 407 newV = cty.NullVal(schema.ImpliedType()) 408 } 409 } 410 411 case configschema.NestingSet: 412 // Nested blocks are correlated by comparing the element values 413 // after eliminating all of the computed attributes. In practice, 414 // this means that any config change produces an entirely new 415 // nested object, and we only propagate prior computed values 416 // if the non-computed attribute values are identical. 417 var cmpVals [][2]cty.Value 418 if prior.IsKnown() && !prior.IsNull() { 419 cmpVals = setElementCompareValuesFromObject(schema, prior) 420 } 421 configVLen := 0 422 if config.IsKnown() && !config.IsNull() { 423 configVLen = config.LengthInt() 424 } 425 if configVLen > 0 { 426 used := make([]bool, len(cmpVals)) // track used elements in case multiple have the same compare value 427 newVals := make([]cty.Value, 0, configVLen) 428 for it := config.ElementIterator(); it.Next(); { 429 _, configEV := it.Element() 430 var priorEV cty.Value 431 for i, cmp := range cmpVals { 432 if used[i] { 433 continue 434 } 435 if cmp[1].RawEquals(configEV) { 436 priorEV = cmp[0] 437 used[i] = true // we can't use this value on a future iteration 438 break 439 } 440 } 441 if priorEV == cty.NilVal { 442 newVals = append(newVals, configEV) 443 } else { 444 newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV) 445 newVals = append(newVals, cty.ObjectVal(newEV)) 446 } 447 } 448 newV = cty.SetVal(newVals) 449 } else { 450 newV = cty.NullVal(schema.ImpliedType()) 451 } 452 } 453 454 return newV 455 } 456 457 // setElementCompareValues takes a known, non-null value of a cty.Set type and 458 // returns a table -- constructed of two-element arrays -- that maps original 459 // set element values to corresponding values that have all of the computed 460 // values removed, making them suitable for comparison with values obtained 461 // from configuration. The element type of the set must conform to the implied 462 // type of the given schema, or this function will panic. 463 // 464 // In the resulting slice, the zeroth element of each array is the original 465 // value and the one-indexed element is the corresponding "compare value". 466 // 467 // This is intended to help correlate prior elements with configured elements 468 // in proposedNewBlock. The result is a heuristic rather than an exact science, 469 // since e.g. two separate elements may reduce to the same value through this 470 // process. The caller must therefore be ready to deal with duplicates. 471 func setElementCompareValues(schema *configschema.Block, set cty.Value, isConfig bool) [][2]cty.Value { 472 ret := make([][2]cty.Value, 0, set.LengthInt()) 473 for it := set.ElementIterator(); it.Next(); { 474 _, ev := it.Element() 475 ret = append(ret, [2]cty.Value{ev, setElementCompareValue(schema, ev, isConfig)}) 476 } 477 return ret 478 } 479 480 // setElementCompareValue creates a new value that has all of the same 481 // non-computed attribute values as the one given but has all computed 482 // attribute values forced to null. 483 // 484 // If isConfig is true then non-null Optional+Computed attribute values will 485 // be preserved. Otherwise, they will also be set to null. 486 // 487 // The input value must conform to the schema's implied type, and the return 488 // value is guaranteed to conform to it. 489 func setElementCompareValue(schema *configschema.Block, v cty.Value, isConfig bool) cty.Value { 490 if v.IsNull() || !v.IsKnown() { 491 return v 492 } 493 494 attrs := map[string]cty.Value{} 495 for name, attr := range schema.Attributes { 496 switch { 497 case attr.Computed && attr.Optional: 498 if isConfig { 499 attrs[name] = v.GetAttr(name) 500 } else { 501 attrs[name] = cty.NullVal(attr.Type) 502 } 503 case attr.Computed: 504 attrs[name] = cty.NullVal(attr.Type) 505 default: 506 attrs[name] = v.GetAttr(name) 507 } 508 } 509 510 for name, blockType := range schema.BlockTypes { 511 elementType := blockType.Block.ImpliedType() 512 513 switch blockType.Nesting { 514 case configschema.NestingSingle, configschema.NestingGroup: 515 attrs[name] = setElementCompareValue(&blockType.Block, v.GetAttr(name), isConfig) 516 517 case configschema.NestingList, configschema.NestingSet: 518 cv := v.GetAttr(name) 519 if cv.IsNull() || !cv.IsKnown() { 520 attrs[name] = cv 521 continue 522 } 523 524 if l := cv.LengthInt(); l > 0 { 525 elems := make([]cty.Value, 0, l) 526 for it := cv.ElementIterator(); it.Next(); { 527 _, ev := it.Element() 528 elems = append(elems, setElementCompareValue(&blockType.Block, ev, isConfig)) 529 } 530 531 switch { 532 case blockType.Nesting == configschema.NestingSet: 533 // SetValEmpty would panic if given elements that are not 534 // all of the same type, but that's guaranteed not to 535 // happen here because our input value was _already_ a 536 // set and we've not changed the types of any elements here. 537 attrs[name] = cty.SetVal(elems) 538 539 // NestingList cases 540 case elementType.HasDynamicTypes(): 541 attrs[name] = cty.TupleVal(elems) 542 default: 543 attrs[name] = cty.ListVal(elems) 544 } 545 } else { 546 switch { 547 case blockType.Nesting == configschema.NestingSet: 548 attrs[name] = cty.SetValEmpty(elementType) 549 550 // NestingList cases 551 case elementType.HasDynamicTypes(): 552 attrs[name] = cty.EmptyTupleVal 553 default: 554 attrs[name] = cty.ListValEmpty(elementType) 555 } 556 } 557 558 case configschema.NestingMap: 559 cv := v.GetAttr(name) 560 if cv.IsNull() || !cv.IsKnown() || cv.LengthInt() == 0 { 561 attrs[name] = cv 562 continue 563 } 564 elems := make(map[string]cty.Value) 565 for it := cv.ElementIterator(); it.Next(); { 566 kv, ev := it.Element() 567 elems[kv.AsString()] = setElementCompareValue(&blockType.Block, ev, isConfig) 568 } 569 570 switch { 571 case elementType.HasDynamicTypes(): 572 attrs[name] = cty.ObjectVal(elems) 573 default: 574 attrs[name] = cty.MapVal(elems) 575 } 576 577 default: 578 // Should never happen, since the above cases are comprehensive. 579 panic(fmt.Sprintf("unsupported block nesting mode %s", blockType.Nesting)) 580 } 581 } 582 583 return cty.ObjectVal(attrs) 584 } 585 586 // setElementCompareValues takes a known, non-null value of a cty.Set type and 587 // returns a table -- constructed of two-element arrays -- that maps original 588 // set element values to corresponding values that have all of the computed 589 // values removed, making them suitable for comparison with values obtained 590 // from configuration. The element type of the set must conform to the implied 591 // type of the given schema, or this function will panic. 592 // 593 // In the resulting slice, the zeroth element of each array is the original 594 // value and the one-indexed element is the corresponding "compare value". 595 // 596 // This is intended to help correlate prior elements with configured elements 597 // in proposedNewBlock. The result is a heuristic rather than an exact science, 598 // since e.g. two separate elements may reduce to the same value through this 599 // process. The caller must therefore be ready to deal with duplicates. 600 func setElementCompareValuesFromObject(schema *configschema.Object, set cty.Value) [][2]cty.Value { 601 ret := make([][2]cty.Value, 0, set.LengthInt()) 602 for it := set.ElementIterator(); it.Next(); { 603 _, ev := it.Element() 604 ret = append(ret, [2]cty.Value{ev, setElementCompareValueFromObject(schema, ev)}) 605 } 606 return ret 607 } 608 609 // setElementCompareValue creates a new value that has all of the same 610 // non-computed attribute values as the one given but has all computed 611 // attribute values forced to null. 612 // 613 // The input value must conform to the schema's implied type, and the return 614 // value is guaranteed to conform to it. 615 func setElementCompareValueFromObject(schema *configschema.Object, v cty.Value) cty.Value { 616 if v.IsNull() || !v.IsKnown() { 617 return v 618 } 619 attrs := map[string]cty.Value{} 620 621 for name, attr := range schema.Attributes { 622 attrV := v.GetAttr(name) 623 switch { 624 case attr.Computed: 625 attrs[name] = cty.NullVal(attr.Type) 626 default: 627 attrs[name] = attrV 628 } 629 } 630 631 return cty.ObjectVal(attrs) 632 }