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