github.com/opentofu/opentofu@v1.7.1/internal/plans/objchange/objchange.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package objchange 7 8 import ( 9 "errors" 10 "fmt" 11 12 "github.com/zclconf/go-cty/cty" 13 14 "github.com/opentofu/opentofu/internal/configs/configschema" 15 ) 16 17 // ProposedNew constructs a proposed new object value by combining the 18 // computed attribute values from "prior" with the configured attribute values 19 // from "config". 20 // 21 // Both value must conform to the given schema's implied type, or this function 22 // will panic. 23 // 24 // The prior value must be wholly known, but the config value may be unknown 25 // or have nested unknown values. 26 // 27 // The merging of the two objects includes the attributes of any nested blocks, 28 // which will be correlated in a manner appropriate for their nesting mode. 29 // Note in particular that the correlation for blocks backed by sets is a 30 // heuristic based on matching non-computed attribute values and so it may 31 // produce strange results with more "extreme" cases, such as a nested set 32 // block where _all_ attributes are computed. 33 func ProposedNew(schema *configschema.Block, prior, config cty.Value) cty.Value { 34 // If the config and prior are both null, return early here before 35 // populating the prior block. The prevents non-null blocks from appearing 36 // the proposed state value. 37 if config.IsNull() && prior.IsNull() { 38 return prior 39 } 40 41 if prior.IsNull() { 42 // In this case, we will construct a synthetic prior value that is 43 // similar to the result of decoding an empty configuration block, 44 // which simplifies our handling of the top-level attributes/blocks 45 // below by giving us one non-null level of object to pull values from. 46 // 47 // "All attributes null" happens to be the definition of EmptyValue for 48 // a Block, so we can just delegate to that 49 prior = schema.EmptyValue() 50 } 51 return proposedNew(schema, prior, config) 52 } 53 54 // PlannedDataResourceObject is similar to proposedNewBlock but tailored for 55 // planning data resources in particular. Specifically, it replaces the values 56 // of any Computed attributes not set in the configuration with an unknown 57 // value, which serves as a placeholder for a value to be filled in by the 58 // provider when the data resource is finally read. 59 // 60 // Data resources are different because the planning of them is handled 61 // entirely within OpenTofu Core and not subject to customization by the 62 // provider. This function is, in effect, producing an equivalent result to 63 // passing the proposedNewBlock result into a provider's PlanResourceChange 64 // function, assuming a fixed implementation of PlanResourceChange that just 65 // fills in unknown values as needed. 66 func PlannedDataResourceObject(schema *configschema.Block, config cty.Value) cty.Value { 67 // Our trick here is to run the proposedNewBlock logic with an 68 // entirely-unknown prior value. Because of cty's unknown short-circuit 69 // behavior, any operation on prior returns another unknown, and so 70 // unknown values propagate into all of the parts of the resulting value 71 // that would normally be filled in by preserving the prior state. 72 prior := cty.UnknownVal(schema.ImpliedType()) 73 return proposedNew(schema, prior, config) 74 } 75 76 func proposedNew(schema *configschema.Block, prior, config cty.Value) cty.Value { 77 if config.IsNull() || !config.IsKnown() { 78 // A block config should never be null at this point. The only nullable 79 // block type is NestingSingle, which will return early before coming 80 // back here. We'll allow the null here anyway to free callers from 81 // needing to specifically check for these cases, and any mismatch will 82 // be caught in validation, so just take the prior value rather than 83 // the invalid null. 84 return prior 85 } 86 87 if (!prior.Type().IsObjectType()) || (!config.Type().IsObjectType()) { 88 panic("ProposedNew only supports object-typed values") 89 } 90 91 // From this point onwards, we can assume that both values are non-null 92 // object types, and that the config value itself is known (though it 93 // may contain nested values that are unknown.) 94 newAttrs := proposedNewAttributes(schema.Attributes, prior, config) 95 96 // Merging nested blocks is a little more complex, since we need to 97 // correlate blocks between both objects and then recursively propose 98 // a new object for each. The correlation logic depends on the nesting 99 // mode for each block type. 100 for name, blockType := range schema.BlockTypes { 101 priorV := prior.GetAttr(name) 102 configV := config.GetAttr(name) 103 newAttrs[name] = proposedNewNestedBlock(blockType, priorV, configV) 104 } 105 106 return cty.ObjectVal(newAttrs) 107 } 108 109 // proposedNewBlockOrObject dispatched the schema to either ProposedNew or 110 // proposedNewObjectAttributes depending on the given type. 111 func proposedNewBlockOrObject(schema nestedSchema, prior, config cty.Value) cty.Value { 112 switch schema := schema.(type) { 113 case *configschema.Block: 114 return ProposedNew(schema, prior, config) 115 case *configschema.Object: 116 return proposedNewObjectAttributes(schema, prior, config) 117 default: 118 panic(fmt.Sprintf("unexpected schema type %T", schema)) 119 } 120 } 121 122 func proposedNewNestedBlock(schema *configschema.NestedBlock, prior, config cty.Value) cty.Value { 123 // The only time we should encounter an entirely unknown block is from the 124 // use of dynamic with an unknown for_each expression. 125 if !config.IsKnown() { 126 return config 127 } 128 129 newV := config 130 131 switch schema.Nesting { 132 case configschema.NestingSingle: 133 // A NestingSingle configuration block value can be null, and since it 134 // cannot be computed we can always take the configuration value. 135 if config.IsNull() { 136 break 137 } 138 139 // Otherwise use the same assignment rules as NestingGroup 140 fallthrough 141 case configschema.NestingGroup: 142 newV = ProposedNew(&schema.Block, prior, config) 143 144 case configschema.NestingList: 145 newV = proposedNewNestingList(&schema.Block, prior, config) 146 147 case configschema.NestingMap: 148 newV = proposedNewNestingMap(&schema.Block, prior, config) 149 150 case configschema.NestingSet: 151 newV = proposedNewNestingSet(&schema.Block, prior, config) 152 153 default: 154 // Should never happen, since the above cases are comprehensive. 155 panic(fmt.Sprintf("unsupported block nesting mode %s", schema.Nesting)) 156 } 157 158 return newV 159 } 160 161 func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value) cty.Value { 162 // if the config isn't known at all, then we must use that value 163 if !config.IsKnown() { 164 return config 165 } 166 167 // Even if the config is null or empty, we will be using this default value. 168 newV := config 169 170 switch schema.Nesting { 171 case configschema.NestingSingle: 172 // If the config is null, we already have our value. If the attribute 173 // is optional+computed, we won't reach this branch with a null value 174 // since the computed case would have been taken. 175 if config.IsNull() { 176 break 177 } 178 179 newV = proposedNewObjectAttributes(schema, prior, config) 180 181 case configschema.NestingList: 182 newV = proposedNewNestingList(schema, prior, config) 183 184 case configschema.NestingMap: 185 newV = proposedNewNestingMap(schema, prior, config) 186 187 case configschema.NestingSet: 188 newV = proposedNewNestingSet(schema, prior, config) 189 190 default: 191 // Should never happen, since the above cases are comprehensive. 192 panic(fmt.Sprintf("unsupported attribute nesting mode %s", schema.Nesting)) 193 } 194 195 return newV 196 } 197 198 func proposedNewNestingList(schema nestedSchema, prior, config cty.Value) cty.Value { 199 newV := config 200 201 // Nested blocks are correlated by index. 202 configVLen := 0 203 if !config.IsNull() { 204 configVLen = config.LengthInt() 205 } 206 if configVLen > 0 { 207 newVals := make([]cty.Value, 0, configVLen) 208 for it := config.ElementIterator(); it.Next(); { 209 idx, configEV := it.Element() 210 if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) { 211 // If there is no corresponding prior element then 212 // we just take the config value as-is. 213 newVals = append(newVals, configEV) 214 continue 215 } 216 priorEV := prior.Index(idx) 217 218 newVals = append(newVals, proposedNewBlockOrObject(schema, priorEV, configEV)) 219 } 220 // Despite the name, a NestingList might also be a tuple, if 221 // its nested schema contains dynamically-typed attributes. 222 if config.Type().IsTupleType() { 223 newV = cty.TupleVal(newVals) 224 } else { 225 newV = cty.ListVal(newVals) 226 } 227 } 228 229 return newV 230 } 231 232 func proposedNewNestingMap(schema nestedSchema, prior, config cty.Value) cty.Value { 233 newV := config 234 235 newVals := map[string]cty.Value{} 236 237 if config.IsNull() || !config.IsKnown() || config.LengthInt() == 0 { 238 // We already assigned newVal and there's nothing to compare in 239 // config. 240 return newV 241 } 242 cfgMap := config.AsValueMap() 243 244 // prior may be null or empty 245 priorMap := map[string]cty.Value{} 246 if !prior.IsNull() && prior.IsKnown() && prior.LengthInt() > 0 { 247 priorMap = prior.AsValueMap() 248 } 249 250 for name, configEV := range cfgMap { 251 priorEV, inPrior := priorMap[name] 252 if !inPrior { 253 // If there is no corresponding prior element then 254 // we just take the config value as-is. 255 newVals[name] = configEV 256 continue 257 } 258 259 newVals[name] = proposedNewBlockOrObject(schema, priorEV, configEV) 260 } 261 262 // The value must leave as the same type it came in as 263 switch { 264 case config.Type().IsObjectType(): 265 // Although we call the nesting mode "map", we actually use 266 // object values so that elements might have different types 267 // in case of dynamically-typed attributes. 268 newV = cty.ObjectVal(newVals) 269 default: 270 newV = cty.MapVal(newVals) 271 } 272 273 return newV 274 } 275 276 func proposedNewNestingSet(schema nestedSchema, prior, config cty.Value) cty.Value { 277 if !config.Type().IsSetType() { 278 panic("configschema.NestingSet value is not a set as expected") 279 } 280 281 newV := config 282 if !config.IsKnown() || config.IsNull() || config.LengthInt() == 0 { 283 return newV 284 } 285 286 var priorVals []cty.Value 287 if prior.IsKnown() && !prior.IsNull() { 288 priorVals = prior.AsValueSlice() 289 } 290 291 var newVals []cty.Value 292 // track which prior elements have been used 293 used := make([]bool, len(priorVals)) 294 295 for _, configEV := range config.AsValueSlice() { 296 var priorEV cty.Value 297 for i, priorCmp := range priorVals { 298 if used[i] { 299 continue 300 } 301 302 // It is possible that multiple prior elements could be valid 303 // matches for a configuration value, in which case we will end up 304 // picking the first match encountered (but it will always be 305 // consistent due to cty's iteration order). Because configured set 306 // elements must also be entirely unique in order to be included in 307 // the set, these matches either will not matter because they only 308 // differ by computed values, or could not have come from a valid 309 // config with all unique set elements. 310 if validPriorFromConfig(schema, priorCmp, configEV) { 311 priorEV = priorCmp 312 used[i] = true 313 break 314 } 315 } 316 317 if priorEV == cty.NilVal { 318 priorEV = cty.NullVal(config.Type().ElementType()) 319 } 320 321 newVals = append(newVals, proposedNewBlockOrObject(schema, priorEV, configEV)) 322 } 323 324 return cty.SetVal(newVals) 325 } 326 327 func proposedNewObjectAttributes(schema *configschema.Object, prior, config cty.Value) cty.Value { 328 if config.IsNull() { 329 return config 330 } 331 332 return cty.ObjectVal(proposedNewAttributes(schema.Attributes, prior, config)) 333 } 334 335 func proposedNewAttributes(attrs map[string]*configschema.Attribute, prior, config cty.Value) map[string]cty.Value { 336 newAttrs := make(map[string]cty.Value, len(attrs)) 337 for name, attr := range attrs { 338 var priorV cty.Value 339 if prior.IsNull() { 340 priorV = cty.NullVal(prior.Type().AttributeType(name)) 341 } else { 342 priorV = prior.GetAttr(name) 343 } 344 345 configV := config.GetAttr(name) 346 347 var newV cty.Value 348 switch { 349 // required isn't considered when constructing the plan, so attributes 350 // are essentially either computed or not computed. In the case of 351 // optional+computed, they are only computed when there is no 352 // configuration. 353 case attr.Computed && configV.IsNull(): 354 // configV will always be null in this case, by definition. 355 // priorV may also be null, but that's okay. 356 newV = priorV 357 358 // the exception to the above is that if the config is optional and 359 // the _prior_ value contains non-computed values, we can infer 360 // that the config must have been non-null previously. 361 if optionalValueNotComputable(attr, priorV) { 362 newV = configV 363 } 364 365 case attr.NestedType != nil: 366 // For non-computed NestedType attributes, we need to descend 367 // into the individual nested attributes to build the final 368 // value, unless the entire nested attribute is unknown. 369 newV = proposedNewNestedType(attr.NestedType, priorV, configV) 370 default: 371 // For non-computed attributes, we always take the config value, 372 // even if it is null. If it's _required_ then null values 373 // should've been caught during an earlier validation step, and 374 // so we don't really care about that here. 375 newV = configV 376 } 377 newAttrs[name] = newV 378 } 379 return newAttrs 380 } 381 382 // nestedSchema is used as a generic container for either a 383 // *configschema.Object, or *configschema.Block. 384 type nestedSchema interface { 385 AttributeByPath(cty.Path) *configschema.Attribute 386 } 387 388 // optionalValueNotComputable is used to check if an object in state must 389 // have at least partially come from configuration. If the prior value has any 390 // non-null attributes which are not computed in the schema, then we know there 391 // was previously a configuration value which set those. 392 // 393 // This is used when the configuration contains a null optional+computed value, 394 // and we want to know if we should plan to send the null value or the prior 395 // state. 396 func optionalValueNotComputable(schema *configschema.Attribute, val cty.Value) bool { 397 if !schema.Optional { 398 return false 399 } 400 401 // We must have a NestedType for complex nested attributes in order 402 // to find nested computed values in the first place. 403 if schema.NestedType == nil { 404 return false 405 } 406 407 foundNonComputedAttr := false 408 cty.Walk(val, func(path cty.Path, v cty.Value) (bool, error) { 409 if v.IsNull() { 410 return true, nil 411 } 412 413 attr := schema.NestedType.AttributeByPath(path) 414 if attr == nil { 415 return true, nil 416 } 417 418 if !attr.Computed { 419 foundNonComputedAttr = true 420 return false, nil 421 } 422 return true, nil 423 }) 424 425 return foundNonComputedAttr 426 } 427 428 // validPriorFromConfig returns true if the prior object could have been 429 // derived from the configuration. We do this by walking the prior value to 430 // determine if it is a valid superset of the config, and only computable 431 // values have been added. This function is only used to correlated 432 // configuration with possible valid prior values within sets. 433 func validPriorFromConfig(schema nestedSchema, prior, config cty.Value) bool { 434 if unrefinedValue(config).RawEquals(unrefinedValue(prior)) { 435 return true 436 } 437 438 // error value to halt the walk 439 stop := errors.New("stop") 440 441 valid := true 442 cty.Walk(prior, func(path cty.Path, priorV cty.Value) (bool, error) { 443 configV, err := path.Apply(config) 444 if err != nil { 445 // most likely dynamic objects with different types 446 valid = false 447 return false, stop 448 } 449 450 // we don't need to know the schema if both are equal 451 if unrefinedValue(configV).RawEquals(unrefinedValue(priorV)) { 452 // we know they are equal, so no need to descend further 453 return false, nil 454 } 455 456 // We can't descend into nested sets to correlate configuration, so the 457 // overall values must be equal. 458 if configV.Type().IsSetType() { 459 valid = false 460 return false, stop 461 } 462 463 attr := schema.AttributeByPath(path) 464 if attr == nil { 465 // Not at a schema attribute, so we can continue until we find leaf 466 // attributes. 467 return true, nil 468 } 469 470 // If we have nested object attributes we'll be descending into those 471 // to compare the individual values and determine why this level is not 472 // equal 473 if attr.NestedType != nil { 474 return true, nil 475 } 476 477 // This is a leaf attribute, so it must be computed in order to differ 478 // from config. 479 if !attr.Computed { 480 valid = false 481 return false, stop 482 } 483 484 // And if it is computed, the config must be null to allow a change. 485 if !configV.IsNull() { 486 valid = false 487 return false, stop 488 } 489 490 // We sill stop here. The cty value could be far larger, but this was 491 // the last level of prescribed schema. 492 return false, nil 493 }) 494 495 return valid 496 }