1package plugin 2 3import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "log" 8 "strconv" 9 10 "github.com/zclconf/go-cty/cty" 11 ctyconvert "github.com/zclconf/go-cty/cty/convert" 12 "github.com/zclconf/go-cty/cty/msgpack" 13 context "golang.org/x/net/context" 14 15 "github.com/hashicorp/terraform/config/hcl2shim" 16 "github.com/hashicorp/terraform/configs/configschema" 17 "github.com/hashicorp/terraform/helper/schema" 18 proto "github.com/hashicorp/terraform/internal/tfplugin5" 19 "github.com/hashicorp/terraform/plugin/convert" 20 "github.com/hashicorp/terraform/terraform" 21) 22 23const newExtraKey = "_new_extra_shim" 24 25// NewGRPCProviderServerShim wraps a terraform.ResourceProvider in a 26// proto.ProviderServer implementation. If the provided provider is not a 27// *schema.Provider, this will return nil, 28func NewGRPCProviderServerShim(p terraform.ResourceProvider) *GRPCProviderServer { 29 sp, ok := p.(*schema.Provider) 30 if !ok { 31 return nil 32 } 33 34 return &GRPCProviderServer{ 35 provider: sp, 36 } 37} 38 39// GRPCProviderServer handles the server, or plugin side of the rpc connection. 40type GRPCProviderServer struct { 41 provider *schema.Provider 42} 43 44func (s *GRPCProviderServer) GetSchema(_ context.Context, req *proto.GetProviderSchema_Request) (*proto.GetProviderSchema_Response, error) { 45 // Here we are certain that the provider is being called through grpc, so 46 // make sure the feature flag for helper/schema is set 47 schema.SetProto5() 48 49 resp := &proto.GetProviderSchema_Response{ 50 ResourceSchemas: make(map[string]*proto.Schema), 51 DataSourceSchemas: make(map[string]*proto.Schema), 52 } 53 54 resp.Provider = &proto.Schema{ 55 Block: convert.ConfigSchemaToProto(s.getProviderSchemaBlock()), 56 } 57 58 for typ, res := range s.provider.ResourcesMap { 59 resp.ResourceSchemas[typ] = &proto.Schema{ 60 Version: int64(res.SchemaVersion), 61 Block: convert.ConfigSchemaToProto(res.CoreConfigSchema()), 62 } 63 } 64 65 for typ, dat := range s.provider.DataSourcesMap { 66 resp.DataSourceSchemas[typ] = &proto.Schema{ 67 Version: int64(dat.SchemaVersion), 68 Block: convert.ConfigSchemaToProto(dat.CoreConfigSchema()), 69 } 70 } 71 72 return resp, nil 73} 74 75func (s *GRPCProviderServer) getProviderSchemaBlock() *configschema.Block { 76 return schema.InternalMap(s.provider.Schema).CoreConfigSchema() 77} 78 79func (s *GRPCProviderServer) getResourceSchemaBlock(name string) *configschema.Block { 80 res := s.provider.ResourcesMap[name] 81 return res.CoreConfigSchema() 82} 83 84func (s *GRPCProviderServer) getDatasourceSchemaBlock(name string) *configschema.Block { 85 dat := s.provider.DataSourcesMap[name] 86 return dat.CoreConfigSchema() 87} 88 89func (s *GRPCProviderServer) PrepareProviderConfig(_ context.Context, req *proto.PrepareProviderConfig_Request) (*proto.PrepareProviderConfig_Response, error) { 90 resp := &proto.PrepareProviderConfig_Response{} 91 92 schemaBlock := s.getProviderSchemaBlock() 93 94 configVal, err := msgpack.Unmarshal(req.Config.Msgpack, schemaBlock.ImpliedType()) 95 if err != nil { 96 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 97 return resp, nil 98 } 99 100 // lookup any required, top-level attributes that are Null, and see if we 101 // have a Default value available. 102 configVal, err = cty.Transform(configVal, func(path cty.Path, val cty.Value) (cty.Value, error) { 103 // we're only looking for top-level attributes 104 if len(path) != 1 { 105 return val, nil 106 } 107 108 // nothing to do if we already have a value 109 if !val.IsNull() { 110 return val, nil 111 } 112 113 // get the Schema definition for this attribute 114 getAttr, ok := path[0].(cty.GetAttrStep) 115 // these should all exist, but just ignore anything strange 116 if !ok { 117 return val, nil 118 } 119 120 attrSchema := s.provider.Schema[getAttr.Name] 121 // continue to ignore anything that doesn't match 122 if attrSchema == nil { 123 return val, nil 124 } 125 126 // this is deprecated, so don't set it 127 if attrSchema.Deprecated != "" || attrSchema.Removed != "" { 128 return val, nil 129 } 130 131 // find a default value if it exists 132 def, err := attrSchema.DefaultValue() 133 if err != nil { 134 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, fmt.Errorf("error getting default for %q: %s", getAttr.Name, err)) 135 return val, err 136 } 137 138 // no default 139 if def == nil { 140 return val, nil 141 } 142 143 // create a cty.Value and make sure it's the correct type 144 tmpVal := hcl2shim.HCL2ValueFromConfigValue(def) 145 146 // helper/schema used to allow setting "" to a bool 147 if val.Type() == cty.Bool && tmpVal.RawEquals(cty.StringVal("")) { 148 // return a warning about the conversion 149 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, "provider set empty string as default value for bool "+getAttr.Name) 150 tmpVal = cty.False 151 } 152 153 val, err = ctyconvert.Convert(tmpVal, val.Type()) 154 if err != nil { 155 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, fmt.Errorf("error setting default for %q: %s", getAttr.Name, err)) 156 } 157 158 return val, err 159 }) 160 if err != nil { 161 // any error here was already added to the diagnostics 162 return resp, nil 163 } 164 165 configVal, err = schemaBlock.CoerceValue(configVal) 166 if err != nil { 167 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 168 return resp, nil 169 } 170 171 // Ensure there are no nulls that will cause helper/schema to panic. 172 if err := validateConfigNulls(configVal, nil); err != nil { 173 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 174 return resp, nil 175 } 176 177 config := terraform.NewResourceConfigShimmed(configVal, schemaBlock) 178 179 warns, errs := s.provider.Validate(config) 180 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, convert.WarnsAndErrsToProto(warns, errs)) 181 182 preparedConfigMP, err := msgpack.Marshal(configVal, schemaBlock.ImpliedType()) 183 if err != nil { 184 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 185 return resp, nil 186 } 187 188 resp.PreparedConfig = &proto.DynamicValue{Msgpack: preparedConfigMP} 189 190 return resp, nil 191} 192 193func (s *GRPCProviderServer) ValidateResourceTypeConfig(_ context.Context, req *proto.ValidateResourceTypeConfig_Request) (*proto.ValidateResourceTypeConfig_Response, error) { 194 resp := &proto.ValidateResourceTypeConfig_Response{} 195 196 schemaBlock := s.getResourceSchemaBlock(req.TypeName) 197 198 configVal, err := msgpack.Unmarshal(req.Config.Msgpack, schemaBlock.ImpliedType()) 199 if err != nil { 200 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 201 return resp, nil 202 } 203 204 config := terraform.NewResourceConfigShimmed(configVal, schemaBlock) 205 206 warns, errs := s.provider.ValidateResource(req.TypeName, config) 207 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, convert.WarnsAndErrsToProto(warns, errs)) 208 209 return resp, nil 210} 211 212func (s *GRPCProviderServer) ValidateDataSourceConfig(_ context.Context, req *proto.ValidateDataSourceConfig_Request) (*proto.ValidateDataSourceConfig_Response, error) { 213 resp := &proto.ValidateDataSourceConfig_Response{} 214 215 schemaBlock := s.getDatasourceSchemaBlock(req.TypeName) 216 217 configVal, err := msgpack.Unmarshal(req.Config.Msgpack, schemaBlock.ImpliedType()) 218 if err != nil { 219 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 220 return resp, nil 221 } 222 223 // Ensure there are no nulls that will cause helper/schema to panic. 224 if err := validateConfigNulls(configVal, nil); err != nil { 225 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 226 return resp, nil 227 } 228 229 config := terraform.NewResourceConfigShimmed(configVal, schemaBlock) 230 231 warns, errs := s.provider.ValidateDataSource(req.TypeName, config) 232 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, convert.WarnsAndErrsToProto(warns, errs)) 233 234 return resp, nil 235} 236 237func (s *GRPCProviderServer) UpgradeResourceState(_ context.Context, req *proto.UpgradeResourceState_Request) (*proto.UpgradeResourceState_Response, error) { 238 resp := &proto.UpgradeResourceState_Response{} 239 240 res := s.provider.ResourcesMap[req.TypeName] 241 schemaBlock := s.getResourceSchemaBlock(req.TypeName) 242 243 version := int(req.Version) 244 245 jsonMap := map[string]interface{}{} 246 var err error 247 248 switch { 249 // We first need to upgrade a flatmap state if it exists. 250 // There should never be both a JSON and Flatmap state in the request. 251 case len(req.RawState.Flatmap) > 0: 252 jsonMap, version, err = s.upgradeFlatmapState(version, req.RawState.Flatmap, res) 253 if err != nil { 254 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 255 return resp, nil 256 } 257 // if there's a JSON state, we need to decode it. 258 case len(req.RawState.Json) > 0: 259 err = json.Unmarshal(req.RawState.Json, &jsonMap) 260 if err != nil { 261 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 262 return resp, nil 263 } 264 default: 265 log.Println("[DEBUG] no state provided to upgrade") 266 return resp, nil 267 } 268 269 // complete the upgrade of the JSON states 270 jsonMap, err = s.upgradeJSONState(version, jsonMap, res) 271 if err != nil { 272 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 273 return resp, nil 274 } 275 276 // The provider isn't required to clean out removed fields 277 s.removeAttributes(jsonMap, schemaBlock.ImpliedType()) 278 279 // now we need to turn the state into the default json representation, so 280 // that it can be re-decoded using the actual schema. 281 val, err := schema.JSONMapToStateValue(jsonMap, schemaBlock) 282 if err != nil { 283 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 284 return resp, nil 285 } 286 287 // encode the final state to the expected msgpack format 288 newStateMP, err := msgpack.Marshal(val, schemaBlock.ImpliedType()) 289 if err != nil { 290 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 291 return resp, nil 292 } 293 294 resp.UpgradedState = &proto.DynamicValue{Msgpack: newStateMP} 295 return resp, nil 296} 297 298// upgradeFlatmapState takes a legacy flatmap state, upgrades it using Migrate 299// state if necessary, and converts it to the new JSON state format decoded as a 300// map[string]interface{}. 301// upgradeFlatmapState returns the json map along with the corresponding schema 302// version. 303func (s *GRPCProviderServer) upgradeFlatmapState(version int, m map[string]string, res *schema.Resource) (map[string]interface{}, int, error) { 304 // this will be the version we've upgraded so, defaulting to the given 305 // version in case no migration was called. 306 upgradedVersion := version 307 308 // first determine if we need to call the legacy MigrateState func 309 requiresMigrate := version < res.SchemaVersion 310 311 schemaType := res.CoreConfigSchema().ImpliedType() 312 313 // if there are any StateUpgraders, then we need to only compare 314 // against the first version there 315 if len(res.StateUpgraders) > 0 { 316 requiresMigrate = version < res.StateUpgraders[0].Version 317 } 318 319 if requiresMigrate { 320 if res.MigrateState == nil { 321 return nil, 0, errors.New("cannot upgrade state, missing MigrateState function") 322 } 323 324 is := &terraform.InstanceState{ 325 ID: m["id"], 326 Attributes: m, 327 Meta: map[string]interface{}{ 328 "schema_version": strconv.Itoa(version), 329 }, 330 } 331 332 is, err := res.MigrateState(version, is, s.provider.Meta()) 333 if err != nil { 334 return nil, 0, err 335 } 336 337 // re-assign the map in case there was a copy made, making sure to keep 338 // the ID 339 m := is.Attributes 340 m["id"] = is.ID 341 342 // if there are further upgraders, then we've only updated that far 343 if len(res.StateUpgraders) > 0 { 344 schemaType = res.StateUpgraders[0].Type 345 upgradedVersion = res.StateUpgraders[0].Version 346 } 347 } else { 348 // the schema version may be newer than the MigrateState functions 349 // handled and older than the current, but still stored in the flatmap 350 // form. If that's the case, we need to find the correct schema type to 351 // convert the state. 352 for _, upgrader := range res.StateUpgraders { 353 if upgrader.Version == version { 354 schemaType = upgrader.Type 355 break 356 } 357 } 358 } 359 360 // now we know the state is up to the latest version that handled the 361 // flatmap format state. Now we can upgrade the format and continue from 362 // there. 363 newConfigVal, err := hcl2shim.HCL2ValueFromFlatmap(m, schemaType) 364 if err != nil { 365 return nil, 0, err 366 } 367 368 jsonMap, err := schema.StateValueToJSONMap(newConfigVal, schemaType) 369 return jsonMap, upgradedVersion, err 370} 371 372func (s *GRPCProviderServer) upgradeJSONState(version int, m map[string]interface{}, res *schema.Resource) (map[string]interface{}, error) { 373 var err error 374 375 for _, upgrader := range res.StateUpgraders { 376 if version != upgrader.Version { 377 continue 378 } 379 380 m, err = upgrader.Upgrade(m, s.provider.Meta()) 381 if err != nil { 382 return nil, err 383 } 384 version++ 385 } 386 387 return m, nil 388} 389 390// Remove any attributes no longer present in the schema, so that the json can 391// be correctly decoded. 392func (s *GRPCProviderServer) removeAttributes(v interface{}, ty cty.Type) { 393 // we're only concerned with finding maps that corespond to object 394 // attributes 395 switch v := v.(type) { 396 case []interface{}: 397 // If these aren't blocks the next call will be a noop 398 if ty.IsListType() || ty.IsSetType() { 399 eTy := ty.ElementType() 400 for _, eV := range v { 401 s.removeAttributes(eV, eTy) 402 } 403 } 404 return 405 case map[string]interface{}: 406 // map blocks aren't yet supported, but handle this just in case 407 if ty.IsMapType() { 408 eTy := ty.ElementType() 409 for _, eV := range v { 410 s.removeAttributes(eV, eTy) 411 } 412 return 413 } 414 415 if ty == cty.DynamicPseudoType { 416 log.Printf("[DEBUG] ignoring dynamic block: %#v\n", v) 417 return 418 } 419 420 if !ty.IsObjectType() { 421 // This shouldn't happen, and will fail to decode further on, so 422 // there's no need to handle it here. 423 log.Printf("[WARN] unexpected type %#v for map in json state", ty) 424 return 425 } 426 427 attrTypes := ty.AttributeTypes() 428 for attr, attrV := range v { 429 attrTy, ok := attrTypes[attr] 430 if !ok { 431 log.Printf("[DEBUG] attribute %q no longer present in schema", attr) 432 delete(v, attr) 433 continue 434 } 435 436 s.removeAttributes(attrV, attrTy) 437 } 438 } 439} 440 441func (s *GRPCProviderServer) Stop(_ context.Context, _ *proto.Stop_Request) (*proto.Stop_Response, error) { 442 resp := &proto.Stop_Response{} 443 444 err := s.provider.Stop() 445 if err != nil { 446 resp.Error = err.Error() 447 } 448 449 return resp, nil 450} 451 452func (s *GRPCProviderServer) Configure(_ context.Context, req *proto.Configure_Request) (*proto.Configure_Response, error) { 453 resp := &proto.Configure_Response{} 454 455 schemaBlock := s.getProviderSchemaBlock() 456 457 configVal, err := msgpack.Unmarshal(req.Config.Msgpack, schemaBlock.ImpliedType()) 458 if err != nil { 459 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 460 return resp, nil 461 } 462 463 s.provider.TerraformVersion = req.TerraformVersion 464 465 // Ensure there are no nulls that will cause helper/schema to panic. 466 if err := validateConfigNulls(configVal, nil); err != nil { 467 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 468 return resp, nil 469 } 470 471 config := terraform.NewResourceConfigShimmed(configVal, schemaBlock) 472 err = s.provider.Configure(config) 473 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 474 475 return resp, nil 476} 477 478func (s *GRPCProviderServer) ReadResource(_ context.Context, req *proto.ReadResource_Request) (*proto.ReadResource_Response, error) { 479 resp := &proto.ReadResource_Response{} 480 481 res := s.provider.ResourcesMap[req.TypeName] 482 schemaBlock := s.getResourceSchemaBlock(req.TypeName) 483 484 stateVal, err := msgpack.Unmarshal(req.CurrentState.Msgpack, schemaBlock.ImpliedType()) 485 if err != nil { 486 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 487 return resp, nil 488 } 489 490 instanceState, err := res.ShimInstanceStateFromValue(stateVal) 491 if err != nil { 492 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 493 return resp, nil 494 } 495 496 newInstanceState, err := res.RefreshWithoutUpgrade(instanceState, s.provider.Meta()) 497 if err != nil { 498 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 499 return resp, nil 500 } 501 502 if newInstanceState == nil || newInstanceState.ID == "" { 503 // The old provider API used an empty id to signal that the remote 504 // object appears to have been deleted, but our new protocol expects 505 // to see a null value (in the cty sense) in that case. 506 newStateMP, err := msgpack.Marshal(cty.NullVal(schemaBlock.ImpliedType()), schemaBlock.ImpliedType()) 507 if err != nil { 508 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 509 } 510 resp.NewState = &proto.DynamicValue{ 511 Msgpack: newStateMP, 512 } 513 return resp, nil 514 } 515 516 // helper/schema should always copy the ID over, but do it again just to be safe 517 newInstanceState.Attributes["id"] = newInstanceState.ID 518 519 newStateVal, err := hcl2shim.HCL2ValueFromFlatmap(newInstanceState.Attributes, schemaBlock.ImpliedType()) 520 if err != nil { 521 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 522 return resp, nil 523 } 524 525 newStateVal = normalizeNullValues(newStateVal, stateVal, false) 526 newStateVal = copyTimeoutValues(newStateVal, stateVal) 527 528 newStateMP, err := msgpack.Marshal(newStateVal, schemaBlock.ImpliedType()) 529 if err != nil { 530 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 531 return resp, nil 532 } 533 534 resp.NewState = &proto.DynamicValue{ 535 Msgpack: newStateMP, 536 } 537 538 return resp, nil 539} 540 541func (s *GRPCProviderServer) PlanResourceChange(_ context.Context, req *proto.PlanResourceChange_Request) (*proto.PlanResourceChange_Response, error) { 542 resp := &proto.PlanResourceChange_Response{} 543 544 // This is a signal to Terraform Core that we're doing the best we can to 545 // shim the legacy type system of the SDK onto the Terraform type system 546 // but we need it to cut us some slack. This setting should not be taken 547 // forward to any new SDK implementations, since setting it prevents us 548 // from catching certain classes of provider bug that can lead to 549 // confusing downstream errors. 550 resp.LegacyTypeSystem = true 551 552 res := s.provider.ResourcesMap[req.TypeName] 553 schemaBlock := s.getResourceSchemaBlock(req.TypeName) 554 555 priorStateVal, err := msgpack.Unmarshal(req.PriorState.Msgpack, schemaBlock.ImpliedType()) 556 if err != nil { 557 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 558 return resp, nil 559 } 560 561 create := priorStateVal.IsNull() 562 563 proposedNewStateVal, err := msgpack.Unmarshal(req.ProposedNewState.Msgpack, schemaBlock.ImpliedType()) 564 if err != nil { 565 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 566 return resp, nil 567 } 568 569 // We don't usually plan destroys, but this can return early in any case. 570 if proposedNewStateVal.IsNull() { 571 resp.PlannedState = req.ProposedNewState 572 return resp, nil 573 } 574 575 info := &terraform.InstanceInfo{ 576 Type: req.TypeName, 577 } 578 579 priorState, err := res.ShimInstanceStateFromValue(priorStateVal) 580 if err != nil { 581 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 582 return resp, nil 583 } 584 priorPrivate := make(map[string]interface{}) 585 if len(req.PriorPrivate) > 0 { 586 if err := json.Unmarshal(req.PriorPrivate, &priorPrivate); err != nil { 587 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 588 return resp, nil 589 } 590 } 591 592 priorState.Meta = priorPrivate 593 594 // Ensure there are no nulls that will cause helper/schema to panic. 595 if err := validateConfigNulls(proposedNewStateVal, nil); err != nil { 596 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 597 return resp, nil 598 } 599 600 // turn the proposed state into a legacy configuration 601 cfg := terraform.NewResourceConfigShimmed(proposedNewStateVal, schemaBlock) 602 603 diff, err := s.provider.SimpleDiff(info, priorState, cfg) 604 if err != nil { 605 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 606 return resp, nil 607 } 608 609 // if this is a new instance, we need to make sure ID is going to be computed 610 if create { 611 if diff == nil { 612 diff = terraform.NewInstanceDiff() 613 } 614 615 diff.Attributes["id"] = &terraform.ResourceAttrDiff{ 616 NewComputed: true, 617 } 618 } 619 620 if diff == nil || len(diff.Attributes) == 0 { 621 // schema.Provider.Diff returns nil if it ends up making a diff with no 622 // changes, but our new interface wants us to return an actual change 623 // description that _shows_ there are no changes. This is always the 624 // prior state, because we force a diff above if this is a new instance. 625 resp.PlannedState = req.PriorState 626 return resp, nil 627 } 628 629 if priorState == nil { 630 priorState = &terraform.InstanceState{} 631 } 632 633 // now we need to apply the diff to the prior state, so get the planned state 634 plannedAttrs, err := diff.Apply(priorState.Attributes, schemaBlock) 635 636 plannedStateVal, err := hcl2shim.HCL2ValueFromFlatmap(plannedAttrs, schemaBlock.ImpliedType()) 637 if err != nil { 638 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 639 return resp, nil 640 } 641 642 plannedStateVal, err = schemaBlock.CoerceValue(plannedStateVal) 643 if err != nil { 644 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 645 return resp, nil 646 } 647 648 plannedStateVal = normalizeNullValues(plannedStateVal, proposedNewStateVal, false) 649 650 if err != nil { 651 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 652 return resp, nil 653 } 654 655 plannedStateVal = copyTimeoutValues(plannedStateVal, proposedNewStateVal) 656 657 // The old SDK code has some imprecisions that cause it to sometimes 658 // generate differences that the SDK itself does not consider significant 659 // but Terraform Core would. To avoid producing weird do-nothing diffs 660 // in that case, we'll check if the provider as produced something we 661 // think is "equivalent" to the prior state and just return the prior state 662 // itself if so, thus ensuring that Terraform Core will treat this as 663 // a no-op. See the docs for ValuesSDKEquivalent for some caveats on its 664 // accuracy. 665 forceNoChanges := false 666 if hcl2shim.ValuesSDKEquivalent(priorStateVal, plannedStateVal) { 667 plannedStateVal = priorStateVal 668 forceNoChanges = true 669 } 670 671 // if this was creating the resource, we need to set any remaining computed 672 // fields 673 if create { 674 plannedStateVal = SetUnknowns(plannedStateVal, schemaBlock) 675 } 676 677 plannedMP, err := msgpack.Marshal(plannedStateVal, schemaBlock.ImpliedType()) 678 if err != nil { 679 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 680 return resp, nil 681 } 682 resp.PlannedState = &proto.DynamicValue{ 683 Msgpack: plannedMP, 684 } 685 686 // Now we need to store any NewExtra values, which are where any actual 687 // StateFunc modified config fields are hidden. 688 privateMap := diff.Meta 689 if privateMap == nil { 690 privateMap = map[string]interface{}{} 691 } 692 693 newExtra := map[string]interface{}{} 694 695 for k, v := range diff.Attributes { 696 if v.NewExtra != nil { 697 newExtra[k] = v.NewExtra 698 } 699 } 700 privateMap[newExtraKey] = newExtra 701 702 // the Meta field gets encoded into PlannedPrivate 703 plannedPrivate, err := json.Marshal(privateMap) 704 if err != nil { 705 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 706 return resp, nil 707 } 708 resp.PlannedPrivate = plannedPrivate 709 710 // collect the attributes that require instance replacement, and convert 711 // them to cty.Paths. 712 var requiresNew []string 713 if !forceNoChanges { 714 for attr, d := range diff.Attributes { 715 if d.RequiresNew { 716 requiresNew = append(requiresNew, attr) 717 } 718 } 719 } 720 721 // If anything requires a new resource already, or the "id" field indicates 722 // that we will be creating a new resource, then we need to add that to 723 // RequiresReplace so that core can tell if the instance is being replaced 724 // even if changes are being suppressed via "ignore_changes". 725 id := plannedStateVal.GetAttr("id") 726 if len(requiresNew) > 0 || id.IsNull() || !id.IsKnown() { 727 requiresNew = append(requiresNew, "id") 728 } 729 730 requiresReplace, err := hcl2shim.RequiresReplace(requiresNew, schemaBlock.ImpliedType()) 731 if err != nil { 732 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 733 return resp, nil 734 } 735 736 // convert these to the protocol structures 737 for _, p := range requiresReplace { 738 resp.RequiresReplace = append(resp.RequiresReplace, pathToAttributePath(p)) 739 } 740 741 return resp, nil 742} 743 744func (s *GRPCProviderServer) ApplyResourceChange(_ context.Context, req *proto.ApplyResourceChange_Request) (*proto.ApplyResourceChange_Response, error) { 745 resp := &proto.ApplyResourceChange_Response{ 746 // Start with the existing state as a fallback 747 NewState: req.PriorState, 748 } 749 750 res := s.provider.ResourcesMap[req.TypeName] 751 schemaBlock := s.getResourceSchemaBlock(req.TypeName) 752 753 priorStateVal, err := msgpack.Unmarshal(req.PriorState.Msgpack, schemaBlock.ImpliedType()) 754 if err != nil { 755 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 756 return resp, nil 757 } 758 759 plannedStateVal, err := msgpack.Unmarshal(req.PlannedState.Msgpack, schemaBlock.ImpliedType()) 760 if err != nil { 761 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 762 return resp, nil 763 } 764 765 info := &terraform.InstanceInfo{ 766 Type: req.TypeName, 767 } 768 769 priorState, err := res.ShimInstanceStateFromValue(priorStateVal) 770 if err != nil { 771 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 772 return resp, nil 773 } 774 775 private := make(map[string]interface{}) 776 if len(req.PlannedPrivate) > 0 { 777 if err := json.Unmarshal(req.PlannedPrivate, &private); err != nil { 778 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 779 return resp, nil 780 } 781 } 782 783 var diff *terraform.InstanceDiff 784 destroy := false 785 786 // a null state means we are destroying the instance 787 if plannedStateVal.IsNull() { 788 destroy = true 789 diff = &terraform.InstanceDiff{ 790 Attributes: make(map[string]*terraform.ResourceAttrDiff), 791 Meta: make(map[string]interface{}), 792 Destroy: true, 793 } 794 } else { 795 diff, err = schema.DiffFromValues(priorStateVal, plannedStateVal, stripResourceModifiers(res)) 796 if err != nil { 797 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 798 return resp, nil 799 } 800 } 801 802 if diff == nil { 803 diff = &terraform.InstanceDiff{ 804 Attributes: make(map[string]*terraform.ResourceAttrDiff), 805 Meta: make(map[string]interface{}), 806 } 807 } 808 809 // add NewExtra Fields that may have been stored in the private data 810 if newExtra := private[newExtraKey]; newExtra != nil { 811 for k, v := range newExtra.(map[string]interface{}) { 812 d := diff.Attributes[k] 813 814 if d == nil { 815 d = &terraform.ResourceAttrDiff{} 816 } 817 818 d.NewExtra = v 819 diff.Attributes[k] = d 820 } 821 } 822 823 if private != nil { 824 diff.Meta = private 825 } 826 827 for k, d := range diff.Attributes { 828 // We need to turn off any RequiresNew. There could be attributes 829 // without changes in here inserted by helper/schema, but if they have 830 // RequiresNew then the state will be dropped from the ResourceData. 831 d.RequiresNew = false 832 833 // Check that any "removed" attributes that don't actually exist in the 834 // prior state, or helper/schema will confuse itself 835 if d.NewRemoved { 836 if _, ok := priorState.Attributes[k]; !ok { 837 delete(diff.Attributes, k) 838 } 839 } 840 } 841 842 newInstanceState, err := s.provider.Apply(info, priorState, diff) 843 // we record the error here, but continue processing any returned state. 844 if err != nil { 845 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 846 } 847 newStateVal := cty.NullVal(schemaBlock.ImpliedType()) 848 849 // Always return a null value for destroy. 850 // While this is usually indicated by a nil state, check for missing ID or 851 // attributes in the case of a provider failure. 852 if destroy || newInstanceState == nil || newInstanceState.Attributes == nil || newInstanceState.ID == "" { 853 newStateMP, err := msgpack.Marshal(newStateVal, schemaBlock.ImpliedType()) 854 if err != nil { 855 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 856 return resp, nil 857 } 858 resp.NewState = &proto.DynamicValue{ 859 Msgpack: newStateMP, 860 } 861 return resp, nil 862 } 863 864 // We keep the null val if we destroyed the resource, otherwise build the 865 // entire object, even if the new state was nil. 866 newStateVal, err = schema.StateValueFromInstanceState(newInstanceState, schemaBlock.ImpliedType()) 867 if err != nil { 868 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 869 return resp, nil 870 } 871 872 newStateVal = normalizeNullValues(newStateVal, plannedStateVal, true) 873 874 newStateVal = copyTimeoutValues(newStateVal, plannedStateVal) 875 876 newStateMP, err := msgpack.Marshal(newStateVal, schemaBlock.ImpliedType()) 877 if err != nil { 878 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 879 return resp, nil 880 } 881 resp.NewState = &proto.DynamicValue{ 882 Msgpack: newStateMP, 883 } 884 885 meta, err := json.Marshal(newInstanceState.Meta) 886 if err != nil { 887 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 888 return resp, nil 889 } 890 resp.Private = meta 891 892 // This is a signal to Terraform Core that we're doing the best we can to 893 // shim the legacy type system of the SDK onto the Terraform type system 894 // but we need it to cut us some slack. This setting should not be taken 895 // forward to any new SDK implementations, since setting it prevents us 896 // from catching certain classes of provider bug that can lead to 897 // confusing downstream errors. 898 resp.LegacyTypeSystem = true 899 900 return resp, nil 901} 902 903func (s *GRPCProviderServer) ImportResourceState(_ context.Context, req *proto.ImportResourceState_Request) (*proto.ImportResourceState_Response, error) { 904 resp := &proto.ImportResourceState_Response{} 905 906 info := &terraform.InstanceInfo{ 907 Type: req.TypeName, 908 } 909 910 newInstanceStates, err := s.provider.ImportState(info, req.Id) 911 if err != nil { 912 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 913 return resp, nil 914 } 915 916 for _, is := range newInstanceStates { 917 // copy the ID again just to be sure it wasn't missed 918 is.Attributes["id"] = is.ID 919 920 resourceType := is.Ephemeral.Type 921 if resourceType == "" { 922 resourceType = req.TypeName 923 } 924 925 schemaBlock := s.getResourceSchemaBlock(resourceType) 926 newStateVal, err := hcl2shim.HCL2ValueFromFlatmap(is.Attributes, schemaBlock.ImpliedType()) 927 if err != nil { 928 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 929 return resp, nil 930 } 931 932 newStateMP, err := msgpack.Marshal(newStateVal, schemaBlock.ImpliedType()) 933 if err != nil { 934 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 935 return resp, nil 936 } 937 938 meta, err := json.Marshal(is.Meta) 939 if err != nil { 940 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 941 return resp, nil 942 } 943 944 importedResource := &proto.ImportResourceState_ImportedResource{ 945 TypeName: resourceType, 946 State: &proto.DynamicValue{ 947 Msgpack: newStateMP, 948 }, 949 Private: meta, 950 } 951 952 resp.ImportedResources = append(resp.ImportedResources, importedResource) 953 } 954 955 return resp, nil 956} 957 958func (s *GRPCProviderServer) ReadDataSource(_ context.Context, req *proto.ReadDataSource_Request) (*proto.ReadDataSource_Response, error) { 959 resp := &proto.ReadDataSource_Response{} 960 961 schemaBlock := s.getDatasourceSchemaBlock(req.TypeName) 962 963 configVal, err := msgpack.Unmarshal(req.Config.Msgpack, schemaBlock.ImpliedType()) 964 if err != nil { 965 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 966 return resp, nil 967 } 968 969 info := &terraform.InstanceInfo{ 970 Type: req.TypeName, 971 } 972 973 // Ensure there are no nulls that will cause helper/schema to panic. 974 if err := validateConfigNulls(configVal, nil); err != nil { 975 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 976 return resp, nil 977 } 978 979 config := terraform.NewResourceConfigShimmed(configVal, schemaBlock) 980 981 // we need to still build the diff separately with the Read method to match 982 // the old behavior 983 diff, err := s.provider.ReadDataDiff(info, config) 984 if err != nil { 985 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 986 return resp, nil 987 } 988 989 // now we can get the new complete data source 990 newInstanceState, err := s.provider.ReadDataApply(info, diff) 991 if err != nil { 992 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 993 return resp, nil 994 } 995 996 newStateVal, err := schema.StateValueFromInstanceState(newInstanceState, schemaBlock.ImpliedType()) 997 if err != nil { 998 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 999 return resp, nil 1000 } 1001 1002 newStateVal = copyTimeoutValues(newStateVal, configVal) 1003 1004 newStateMP, err := msgpack.Marshal(newStateVal, schemaBlock.ImpliedType()) 1005 if err != nil { 1006 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) 1007 return resp, nil 1008 } 1009 resp.State = &proto.DynamicValue{ 1010 Msgpack: newStateMP, 1011 } 1012 return resp, nil 1013} 1014 1015func pathToAttributePath(path cty.Path) *proto.AttributePath { 1016 var steps []*proto.AttributePath_Step 1017 1018 for _, step := range path { 1019 switch s := step.(type) { 1020 case cty.GetAttrStep: 1021 steps = append(steps, &proto.AttributePath_Step{ 1022 Selector: &proto.AttributePath_Step_AttributeName{ 1023 AttributeName: s.Name, 1024 }, 1025 }) 1026 case cty.IndexStep: 1027 ty := s.Key.Type() 1028 switch ty { 1029 case cty.Number: 1030 i, _ := s.Key.AsBigFloat().Int64() 1031 steps = append(steps, &proto.AttributePath_Step{ 1032 Selector: &proto.AttributePath_Step_ElementKeyInt{ 1033 ElementKeyInt: i, 1034 }, 1035 }) 1036 case cty.String: 1037 steps = append(steps, &proto.AttributePath_Step{ 1038 Selector: &proto.AttributePath_Step_ElementKeyString{ 1039 ElementKeyString: s.Key.AsString(), 1040 }, 1041 }) 1042 } 1043 } 1044 } 1045 1046 return &proto.AttributePath{Steps: steps} 1047} 1048 1049// helper/schema throws away timeout values from the config and stores them in 1050// the Private/Meta fields. we need to copy those values into the planned state 1051// so that core doesn't see a perpetual diff with the timeout block. 1052func copyTimeoutValues(to cty.Value, from cty.Value) cty.Value { 1053 // if `to` is null we are planning to remove it altogether. 1054 if to.IsNull() { 1055 return to 1056 } 1057 toAttrs := to.AsValueMap() 1058 // We need to remove the key since the hcl2shims will add a non-null block 1059 // because we can't determine if a single block was null from the flatmapped 1060 // values. This needs to conform to the correct schema for marshaling, so 1061 // change the value to null rather than deleting it from the object map. 1062 timeouts, ok := toAttrs[schema.TimeoutsConfigKey] 1063 if ok { 1064 toAttrs[schema.TimeoutsConfigKey] = cty.NullVal(timeouts.Type()) 1065 } 1066 1067 // if from is null then there are no timeouts to copy 1068 if from.IsNull() { 1069 return cty.ObjectVal(toAttrs) 1070 } 1071 1072 fromAttrs := from.AsValueMap() 1073 timeouts, ok = fromAttrs[schema.TimeoutsConfigKey] 1074 1075 // timeouts shouldn't be unknown, but don't copy possibly invalid values either 1076 if !ok || timeouts.IsNull() || !timeouts.IsWhollyKnown() { 1077 // no timeouts block to copy 1078 return cty.ObjectVal(toAttrs) 1079 } 1080 1081 toAttrs[schema.TimeoutsConfigKey] = timeouts 1082 1083 return cty.ObjectVal(toAttrs) 1084} 1085 1086// stripResourceModifiers takes a *schema.Resource and returns a deep copy with all 1087// StateFuncs and CustomizeDiffs removed. This will be used during apply to 1088// create a diff from a planned state where the diff modifications have already 1089// been applied. 1090func stripResourceModifiers(r *schema.Resource) *schema.Resource { 1091 if r == nil { 1092 return nil 1093 } 1094 // start with a shallow copy 1095 newResource := new(schema.Resource) 1096 *newResource = *r 1097 1098 newResource.CustomizeDiff = nil 1099 newResource.Schema = map[string]*schema.Schema{} 1100 1101 for k, s := range r.Schema { 1102 newResource.Schema[k] = stripSchema(s) 1103 } 1104 1105 return newResource 1106} 1107 1108func stripSchema(s *schema.Schema) *schema.Schema { 1109 if s == nil { 1110 return nil 1111 } 1112 // start with a shallow copy 1113 newSchema := new(schema.Schema) 1114 *newSchema = *s 1115 1116 newSchema.StateFunc = nil 1117 1118 switch e := newSchema.Elem.(type) { 1119 case *schema.Schema: 1120 newSchema.Elem = stripSchema(e) 1121 case *schema.Resource: 1122 newSchema.Elem = stripResourceModifiers(e) 1123 } 1124 1125 return newSchema 1126} 1127 1128// Zero values and empty containers may be interchanged by the apply process. 1129// When there is a discrepency between src and dst value being null or empty, 1130// prefer the src value. This takes a little more liberty with set types, since 1131// we can't correlate modified set values. In the case of sets, if the src set 1132// was wholly known we assume the value was correctly applied and copy that 1133// entirely to the new value. 1134// While apply prefers the src value, during plan we prefer dst whenever there 1135// is an unknown or a set is involved, since the plan can alter the value 1136// however it sees fit. This however means that a CustomizeDiffFunction may not 1137// be able to change a null to an empty value or vice versa, but that should be 1138// very uncommon nor was it reliable before 0.12 either. 1139func normalizeNullValues(dst, src cty.Value, apply bool) cty.Value { 1140 ty := dst.Type() 1141 if !src.IsNull() && !src.IsKnown() { 1142 // Return src during plan to retain unknown interpolated placeholders, 1143 // which could be lost if we're only updating a resource. If this is a 1144 // read scenario, then there shouldn't be any unknowns at all. 1145 if dst.IsNull() && !apply { 1146 return src 1147 } 1148 return dst 1149 } 1150 1151 // Handle null/empty changes for collections during apply. 1152 // A change between null and empty values prefers src to make sure the state 1153 // is consistent between plan and apply. 1154 if ty.IsCollectionType() && apply { 1155 dstEmpty := !dst.IsNull() && dst.IsKnown() && dst.LengthInt() == 0 1156 srcEmpty := !src.IsNull() && src.IsKnown() && src.LengthInt() == 0 1157 1158 if (src.IsNull() && dstEmpty) || (srcEmpty && dst.IsNull()) { 1159 return src 1160 } 1161 } 1162 1163 if src.IsNull() || !src.IsKnown() || !dst.IsKnown() { 1164 return dst 1165 } 1166 1167 switch { 1168 case ty.IsMapType(), ty.IsObjectType(): 1169 var dstMap map[string]cty.Value 1170 if !dst.IsNull() { 1171 dstMap = dst.AsValueMap() 1172 } 1173 if dstMap == nil { 1174 dstMap = map[string]cty.Value{} 1175 } 1176 1177 srcMap := src.AsValueMap() 1178 for key, v := range srcMap { 1179 dstVal, ok := dstMap[key] 1180 if !ok && apply && ty.IsMapType() { 1181 // don't transfer old map values to dst during apply 1182 continue 1183 } 1184 1185 if dstVal == cty.NilVal { 1186 if !apply && ty.IsMapType() { 1187 // let plan shape this map however it wants 1188 continue 1189 } 1190 dstVal = cty.NullVal(v.Type()) 1191 } 1192 1193 dstMap[key] = normalizeNullValues(dstVal, v, apply) 1194 } 1195 1196 // you can't call MapVal/ObjectVal with empty maps, but nothing was 1197 // copied in anyway. If the dst is nil, and the src is known, assume the 1198 // src is correct. 1199 if len(dstMap) == 0 { 1200 if dst.IsNull() && src.IsWhollyKnown() && apply { 1201 return src 1202 } 1203 return dst 1204 } 1205 1206 if ty.IsMapType() { 1207 // helper/schema will populate an optional+computed map with 1208 // unknowns which we have to fixup here. 1209 // It would be preferable to simply prevent any known value from 1210 // becoming unknown, but concessions have to be made to retain the 1211 // broken legacy behavior when possible. 1212 for k, srcVal := range srcMap { 1213 if !srcVal.IsNull() && srcVal.IsKnown() { 1214 dstVal, ok := dstMap[k] 1215 if !ok { 1216 continue 1217 } 1218 1219 if !dstVal.IsNull() && !dstVal.IsKnown() { 1220 dstMap[k] = srcVal 1221 } 1222 } 1223 } 1224 1225 return cty.MapVal(dstMap) 1226 } 1227 1228 return cty.ObjectVal(dstMap) 1229 1230 case ty.IsSetType(): 1231 // If the original was wholly known, then we expect that is what the 1232 // provider applied. The apply process loses too much information to 1233 // reliably re-create the set. 1234 if src.IsWhollyKnown() && apply { 1235 return src 1236 } 1237 1238 case ty.IsListType(), ty.IsTupleType(): 1239 // If the dst is null, and the src is known, then we lost an empty value 1240 // so take the original. 1241 if dst.IsNull() { 1242 if src.IsWhollyKnown() && src.LengthInt() == 0 && apply { 1243 return src 1244 } 1245 1246 // if dst is null and src only contains unknown values, then we lost 1247 // those during a read or plan. 1248 if !apply && !src.IsNull() { 1249 allUnknown := true 1250 for _, v := range src.AsValueSlice() { 1251 if v.IsKnown() { 1252 allUnknown = false 1253 break 1254 } 1255 } 1256 if allUnknown { 1257 return src 1258 } 1259 } 1260 1261 return dst 1262 } 1263 1264 // if the lengths are identical, then iterate over each element in succession. 1265 srcLen := src.LengthInt() 1266 dstLen := dst.LengthInt() 1267 if srcLen == dstLen && srcLen > 0 { 1268 srcs := src.AsValueSlice() 1269 dsts := dst.AsValueSlice() 1270 1271 for i := 0; i < srcLen; i++ { 1272 dsts[i] = normalizeNullValues(dsts[i], srcs[i], apply) 1273 } 1274 1275 if ty.IsTupleType() { 1276 return cty.TupleVal(dsts) 1277 } 1278 return cty.ListVal(dsts) 1279 } 1280 1281 case ty.IsPrimitiveType(): 1282 if dst.IsNull() && src.IsWhollyKnown() && apply { 1283 return src 1284 } 1285 } 1286 1287 return dst 1288} 1289 1290// validateConfigNulls checks a config value for unsupported nulls before 1291// attempting to shim the value. While null values can mostly be ignored in the 1292// configuration, since they're not supported in HCL1, the case where a null 1293// appears in a list-like attribute (list, set, tuple) will present a nil value 1294// to helper/schema which can panic. Return an error to the user in this case, 1295// indicating the attribute with the null value. 1296func validateConfigNulls(v cty.Value, path cty.Path) []*proto.Diagnostic { 1297 var diags []*proto.Diagnostic 1298 if v.IsNull() || !v.IsKnown() { 1299 return diags 1300 } 1301 1302 switch { 1303 case v.Type().IsListType() || v.Type().IsSetType() || v.Type().IsTupleType(): 1304 it := v.ElementIterator() 1305 for it.Next() { 1306 kv, ev := it.Element() 1307 if ev.IsNull() { 1308 diags = append(diags, &proto.Diagnostic{ 1309 Severity: proto.Diagnostic_ERROR, 1310 Summary: "Null value found in list", 1311 Detail: "Null values are not allowed for this attribute value.", 1312 Attribute: convert.PathToAttributePath(append(path, cty.IndexStep{Key: kv})), 1313 }) 1314 continue 1315 } 1316 1317 d := validateConfigNulls(ev, append(path, cty.IndexStep{Key: kv})) 1318 diags = convert.AppendProtoDiag(diags, d) 1319 } 1320 1321 case v.Type().IsMapType() || v.Type().IsObjectType(): 1322 it := v.ElementIterator() 1323 for it.Next() { 1324 kv, ev := it.Element() 1325 var step cty.PathStep 1326 switch { 1327 case v.Type().IsMapType(): 1328 step = cty.IndexStep{Key: kv} 1329 case v.Type().IsObjectType(): 1330 step = cty.GetAttrStep{Name: kv.AsString()} 1331 } 1332 d := validateConfigNulls(ev, append(path, step)) 1333 diags = convert.AppendProtoDiag(diags, d) 1334 } 1335 } 1336 1337 return diags 1338} 1339