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