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