10 "github.com/zclconf/go-cty/cty"
11 ctyconvert "github.com/zclconf/go-cty/cty/convert"
12 "github.com/zclconf/go-cty/cty/msgpack"
13 context "golang.org/x/net/context"
15 "github.com/hashicorp/terraform/config/hcl2shim"
16 "github.com/hashicorp/terraform/configs/configschema"
17 "github.com/hashicorp/terraform/helper/schema"
18 proto "github.com/hashicorp/terraform/internal/tfplugin5"
19 "github.com/hashicorp/terraform/plugin/convert"
20 "github.com/hashicorp/terraform/terraform"
23 const newExtraKey = "_new_extra_shim"
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)
34 return &GRPCProviderServer{
39 // GRPCProviderServer handles the server, or plugin side of the rpc connection.
40 type GRPCProviderServer struct {
41 provider *schema.Provider
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
49 resp := &proto.GetProviderSchema_Response{
50 ResourceSchemas: make(map[string]*proto.Schema),
51 DataSourceSchemas: make(map[string]*proto.Schema),
54 resp.Provider = &proto.Schema{
55 Block: convert.ConfigSchemaToProto(s.getProviderSchemaBlock()),
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()),
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()),
75 func (s *GRPCProviderServer) getProviderSchemaBlock() *configschema.Block {
76 return schema.InternalMap(s.provider.Schema).CoreConfigSchema()
79 func (s *GRPCProviderServer) getResourceSchemaBlock(name string) *configschema.Block {
80 res := s.provider.ResourcesMap[name]
81 return res.CoreConfigSchema()
84 func (s *GRPCProviderServer) getDatasourceSchemaBlock(name string) *configschema.Block {
85 dat := s.provider.DataSourcesMap[name]
86 return dat.CoreConfigSchema()
89 func (s *GRPCProviderServer) PrepareProviderConfig(_ context.Context, req *proto.PrepareProviderConfig_Request) (*proto.PrepareProviderConfig_Response, error) {
90 resp := &proto.PrepareProviderConfig_Response{}
92 schemaBlock := s.getProviderSchemaBlock()
94 configVal, err := msgpack.Unmarshal(req.Config.Msgpack, schemaBlock.ImpliedType())
96 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
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
108 // nothing to do if we already have a value
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
120 attrSchema := s.provider.Schema[getAttr.Name]
121 // continue to ignore anything that doesn't match
122 if attrSchema == nil {
126 // this is deprecated, so don't set it
127 if attrSchema.Deprecated != "" || attrSchema.Removed != "" {
131 // find a default value if it exists
132 def, err := attrSchema.DefaultValue()
134 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, fmt.Errorf("error getting default for %q: %s", getAttr.Name, err))
143 // create a cty.Value and make sure it's the correct type
144 tmpVal := hcl2shim.HCL2ValueFromConfigValue(def)
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)
153 val, err = ctyconvert.Convert(tmpVal, val.Type())
155 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, fmt.Errorf("error setting default for %q: %s", getAttr.Name, err))
161 // any error here was already added to the diagnostics
165 configVal, err = schemaBlock.CoerceValue(configVal)
167 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
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)
177 config := terraform.NewResourceConfigShimmed(configVal, schemaBlock)
179 warns, errs := s.provider.Validate(config)
180 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, convert.WarnsAndErrsToProto(warns, errs))
182 preparedConfigMP, err := msgpack.Marshal(configVal, schemaBlock.ImpliedType())
184 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
188 resp.PreparedConfig = &proto.DynamicValue{Msgpack: preparedConfigMP}
193 func (s *GRPCProviderServer) ValidateResourceTypeConfig(_ context.Context, req *proto.ValidateResourceTypeConfig_Request) (*proto.ValidateResourceTypeConfig_Response, error) {
194 resp := &proto.ValidateResourceTypeConfig_Response{}
196 schemaBlock := s.getResourceSchemaBlock(req.TypeName)
198 configVal, err := msgpack.Unmarshal(req.Config.Msgpack, schemaBlock.ImpliedType())
200 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
204 config := terraform.NewResourceConfigShimmed(configVal, schemaBlock)
206 warns, errs := s.provider.ValidateResource(req.TypeName, config)
207 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, convert.WarnsAndErrsToProto(warns, errs))
212 func (s *GRPCProviderServer) ValidateDataSourceConfig(_ context.Context, req *proto.ValidateDataSourceConfig_Request) (*proto.ValidateDataSourceConfig_Response, error) {
213 resp := &proto.ValidateDataSourceConfig_Response{}
215 schemaBlock := s.getDatasourceSchemaBlock(req.TypeName)
217 configVal, err := msgpack.Unmarshal(req.Config.Msgpack, schemaBlock.ImpliedType())
219 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
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)
229 config := terraform.NewResourceConfigShimmed(configVal, schemaBlock)
231 warns, errs := s.provider.ValidateDataSource(req.TypeName, config)
232 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, convert.WarnsAndErrsToProto(warns, errs))
237 func (s *GRPCProviderServer) UpgradeResourceState(_ context.Context, req *proto.UpgradeResourceState_Request) (*proto.UpgradeResourceState_Response, error) {
238 resp := &proto.UpgradeResourceState_Response{}
240 res := s.provider.ResourcesMap[req.TypeName]
241 schemaBlock := s.getResourceSchemaBlock(req.TypeName)
243 version := int(req.Version)
245 jsonMap := map[string]interface{}{}
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)
254 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
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)
261 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
265 log.Println("[DEBUG] no state provided to upgrade")
269 // complete the upgrade of the JSON states
270 jsonMap, err = s.upgradeJSONState(version, jsonMap, res)
272 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
276 // The provider isn't required to clean out removed fields
277 s.removeAttributes(jsonMap, schemaBlock.ImpliedType())
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)
283 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
287 // encode the final state to the expected msgpack format
288 newStateMP, err := msgpack.Marshal(val, schemaBlock.ImpliedType())
290 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
294 resp.UpgradedState = &proto.DynamicValue{Msgpack: newStateMP}
298 // upgradeFlatmapState takes a legacy flatmap state, upgrades it using Migrate
299 // state if necessary, and converts it to the new JSON state format decoded as a
300 // map[string]interface{}.
301 // upgradeFlatmapState returns the json map along with the corresponding schema
303 func (s *GRPCProviderServer) upgradeFlatmapState(version int, m map[string]string, res *schema.Resource) (map[string]interface{}, int, error) {
304 // this will be the version we've upgraded so, defaulting to the given
305 // version in case no migration was called.
306 upgradedVersion := version
308 // first determine if we need to call the legacy MigrateState func
309 requiresMigrate := version < res.SchemaVersion
311 schemaType := res.CoreConfigSchema().ImpliedType()
313 // if there are any StateUpgraders, then we need to only compare
314 // against the first version there
315 if len(res.StateUpgraders) > 0 {
316 requiresMigrate = version < res.StateUpgraders[0].Version
320 if res.MigrateState == nil {
321 return nil, 0, errors.New("cannot upgrade state, missing MigrateState function")
324 is := &terraform.InstanceState{
327 Meta: map[string]interface{}{
328 "schema_version": strconv.Itoa(version),
332 is, err := res.MigrateState(version, is, s.provider.Meta())
337 // re-assign the map in case there was a copy made, making sure to keep
342 // if there are further upgraders, then we've only updated that far
343 if len(res.StateUpgraders) > 0 {
344 schemaType = res.StateUpgraders[0].Type
345 upgradedVersion = res.StateUpgraders[0].Version
348 // the schema version may be newer than the MigrateState functions
349 // handled and older than the current, but still stored in the flatmap
350 // form. If that's the case, we need to find the correct schema type to
351 // convert the state.
352 for _, upgrader := range res.StateUpgraders {
353 if upgrader.Version == version {
354 schemaType = upgrader.Type
360 // now we know the state is up to the latest version that handled the
361 // flatmap format state. Now we can upgrade the format and continue from
363 newConfigVal, err := hcl2shim.HCL2ValueFromFlatmap(m, schemaType)
368 jsonMap, err := schema.StateValueToJSONMap(newConfigVal, schemaType)
369 return jsonMap, upgradedVersion, err
372 func (s *GRPCProviderServer) upgradeJSONState(version int, m map[string]interface{}, res *schema.Resource) (map[string]interface{}, error) {
375 for _, upgrader := range res.StateUpgraders {
376 if version != upgrader.Version {
380 m, err = upgrader.Upgrade(m, s.provider.Meta())
390 // Remove any attributes no longer present in the schema, so that the json can
391 // be correctly decoded.
392 func (s *GRPCProviderServer) removeAttributes(v interface{}, ty cty.Type) {
393 // we're only concerned with finding maps that corespond to object
395 switch v := v.(type) {
397 // If these aren't blocks the next call will be a noop
398 if ty.IsListType() || ty.IsSetType() {
399 eTy := ty.ElementType()
400 for _, eV := range v {
401 s.removeAttributes(eV, eTy)
405 case map[string]interface{}:
406 // map blocks aren't yet supported, but handle this just in case
408 eTy := ty.ElementType()
409 for _, eV := range v {
410 s.removeAttributes(eV, eTy)
415 if ty == cty.DynamicPseudoType {
416 log.Printf("[DEBUG] ignoring dynamic block: %#v\n", v)
420 if !ty.IsObjectType() {
421 // This shouldn't happen, and will fail to decode further on, so
422 // there's no need to handle it here.
423 log.Printf("[WARN] unexpected type %#v for map in json state", ty)
427 attrTypes := ty.AttributeTypes()
428 for attr, attrV := range v {
429 attrTy, ok := attrTypes[attr]
431 log.Printf("[DEBUG] attribute %q no longer present in schema", attr)
436 s.removeAttributes(attrV, attrTy)
441 func (s *GRPCProviderServer) Stop(_ context.Context, _ *proto.Stop_Request) (*proto.Stop_Response, error) {
442 resp := &proto.Stop_Response{}
444 err := s.provider.Stop()
446 resp.Error = err.Error()
452 func (s *GRPCProviderServer) Configure(_ context.Context, req *proto.Configure_Request) (*proto.Configure_Response, error) {
453 resp := &proto.Configure_Response{}
455 schemaBlock := s.getProviderSchemaBlock()
457 configVal, err := msgpack.Unmarshal(req.Config.Msgpack, schemaBlock.ImpliedType())
459 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
463 s.provider.TerraformVersion = req.TerraformVersion
465 // Ensure there are no nulls that will cause helper/schema to panic.
466 if err := validateConfigNulls(configVal, nil); err != nil {
467 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
471 config := terraform.NewResourceConfigShimmed(configVal, schemaBlock)
472 err = s.provider.Configure(config)
473 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
478 func (s *GRPCProviderServer) ReadResource(_ context.Context, req *proto.ReadResource_Request) (*proto.ReadResource_Response, error) {
479 resp := &proto.ReadResource_Response{}
481 res := s.provider.ResourcesMap[req.TypeName]
482 schemaBlock := s.getResourceSchemaBlock(req.TypeName)
484 stateVal, err := msgpack.Unmarshal(req.CurrentState.Msgpack, schemaBlock.ImpliedType())
486 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
490 instanceState, err := res.ShimInstanceStateFromValue(stateVal)
492 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
496 newInstanceState, err := res.RefreshWithoutUpgrade(instanceState, s.provider.Meta())
498 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
502 if newInstanceState == nil || newInstanceState.ID == "" {
503 // The old provider API used an empty id to signal that the remote
504 // object appears to have been deleted, but our new protocol expects
505 // to see a null value (in the cty sense) in that case.
506 newStateMP, err := msgpack.Marshal(cty.NullVal(schemaBlock.ImpliedType()), schemaBlock.ImpliedType())
508 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
510 resp.NewState = &proto.DynamicValue{
516 // helper/schema should always copy the ID over, but do it again just to be safe
517 newInstanceState.Attributes["id"] = newInstanceState.ID
519 newStateVal, err := hcl2shim.HCL2ValueFromFlatmap(newInstanceState.Attributes, schemaBlock.ImpliedType())
521 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
525 newStateVal = normalizeNullValues(newStateVal, stateVal, false)
526 newStateVal = copyTimeoutValues(newStateVal, stateVal)
528 newStateMP, err := msgpack.Marshal(newStateVal, schemaBlock.ImpliedType())
530 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
534 resp.NewState = &proto.DynamicValue{
541 func (s *GRPCProviderServer) PlanResourceChange(_ context.Context, req *proto.PlanResourceChange_Request) (*proto.PlanResourceChange_Response, error) {
542 resp := &proto.PlanResourceChange_Response{}
544 // This is a signal to Terraform Core that we're doing the best we can to
545 // shim the legacy type system of the SDK onto the Terraform type system
546 // but we need it to cut us some slack. This setting should not be taken
547 // forward to any new SDK implementations, since setting it prevents us
548 // from catching certain classes of provider bug that can lead to
549 // confusing downstream errors.
550 resp.LegacyTypeSystem = true
552 res := s.provider.ResourcesMap[req.TypeName]
553 schemaBlock := s.getResourceSchemaBlock(req.TypeName)
555 priorStateVal, err := msgpack.Unmarshal(req.PriorState.Msgpack, schemaBlock.ImpliedType())
557 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
561 create := priorStateVal.IsNull()
563 proposedNewStateVal, err := msgpack.Unmarshal(req.ProposedNewState.Msgpack, schemaBlock.ImpliedType())
565 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
569 // We don't usually plan destroys, but this can return early in any case.
570 if proposedNewStateVal.IsNull() {
571 resp.PlannedState = req.ProposedNewState
575 info := &terraform.InstanceInfo{
579 priorState, err := res.ShimInstanceStateFromValue(priorStateVal)
581 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
584 priorPrivate := make(map[string]interface{})
585 if len(req.PriorPrivate) > 0 {
586 if err := json.Unmarshal(req.PriorPrivate, &priorPrivate); err != nil {
587 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
592 priorState.Meta = priorPrivate
594 // Ensure there are no nulls that will cause helper/schema to panic.
595 if err := validateConfigNulls(proposedNewStateVal, nil); err != nil {
596 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
600 // turn the proposed state into a legacy configuration
601 cfg := terraform.NewResourceConfigShimmed(proposedNewStateVal, schemaBlock)
603 diff, err := s.provider.SimpleDiff(info, priorState, cfg)
605 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
609 // if this is a new instance, we need to make sure ID is going to be computed
612 diff = terraform.NewInstanceDiff()
615 diff.Attributes["id"] = &terraform.ResourceAttrDiff{
620 if diff == nil || len(diff.Attributes) == 0 {
621 // schema.Provider.Diff returns nil if it ends up making a diff with no
622 // changes, but our new interface wants us to return an actual change
623 // description that _shows_ there are no changes. This is always the
624 // prior state, because we force a diff above if this is a new instance.
625 resp.PlannedState = req.PriorState
629 if priorState == nil {
630 priorState = &terraform.InstanceState{}
633 // now we need to apply the diff to the prior state, so get the planned state
634 plannedAttrs, err := diff.Apply(priorState.Attributes, schemaBlock)
636 plannedStateVal, err := hcl2shim.HCL2ValueFromFlatmap(plannedAttrs, schemaBlock.ImpliedType())
638 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
642 plannedStateVal, err = schemaBlock.CoerceValue(plannedStateVal)
644 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
648 plannedStateVal = normalizeNullValues(plannedStateVal, proposedNewStateVal, false)
651 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
655 plannedStateVal = copyTimeoutValues(plannedStateVal, proposedNewStateVal)
657 // The old SDK code has some imprecisions that cause it to sometimes
658 // generate differences that the SDK itself does not consider significant
659 // but Terraform Core would. To avoid producing weird do-nothing diffs
660 // in that case, we'll check if the provider as produced something we
661 // think is "equivalent" to the prior state and just return the prior state
662 // itself if so, thus ensuring that Terraform Core will treat this as
663 // a no-op. See the docs for ValuesSDKEquivalent for some caveats on its
665 forceNoChanges := false
666 if hcl2shim.ValuesSDKEquivalent(priorStateVal, plannedStateVal) {
667 plannedStateVal = priorStateVal
668 forceNoChanges = true
671 // if this was creating the resource, we need to set any remaining computed
674 plannedStateVal = SetUnknowns(plannedStateVal, schemaBlock)
677 plannedMP, err := msgpack.Marshal(plannedStateVal, schemaBlock.ImpliedType())
679 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
682 resp.PlannedState = &proto.DynamicValue{
686 // Now we need to store any NewExtra values, which are where any actual
687 // StateFunc modified config fields are hidden.
688 privateMap := diff.Meta
689 if privateMap == nil {
690 privateMap = map[string]interface{}{}
693 newExtra := map[string]interface{}{}
695 for k, v := range diff.Attributes {
696 if v.NewExtra != nil {
697 newExtra[k] = v.NewExtra
700 privateMap[newExtraKey] = newExtra
702 // the Meta field gets encoded into PlannedPrivate
703 plannedPrivate, err := json.Marshal(privateMap)
705 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
708 resp.PlannedPrivate = plannedPrivate
710 // collect the attributes that require instance replacement, and convert
711 // them to cty.Paths.
712 var requiresNew []string
714 for attr, d := range diff.Attributes {
716 requiresNew = append(requiresNew, attr)
721 // If anything requires a new resource already, or the "id" field indicates
722 // that we will be creating a new resource, then we need to add that to
723 // RequiresReplace so that core can tell if the instance is being replaced
724 // even if changes are being suppressed via "ignore_changes".
725 id := plannedStateVal.GetAttr("id")
726 if len(requiresNew) > 0 || id.IsNull() || !id.IsKnown() {
727 requiresNew = append(requiresNew, "id")
730 requiresReplace, err := hcl2shim.RequiresReplace(requiresNew, schemaBlock.ImpliedType())
732 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
736 // convert these to the protocol structures
737 for _, p := range requiresReplace {
738 resp.RequiresReplace = append(resp.RequiresReplace, pathToAttributePath(p))
744 func (s *GRPCProviderServer) ApplyResourceChange(_ context.Context, req *proto.ApplyResourceChange_Request) (*proto.ApplyResourceChange_Response, error) {
745 resp := &proto.ApplyResourceChange_Response{
746 // Start with the existing state as a fallback
747 NewState: req.PriorState,
750 res := s.provider.ResourcesMap[req.TypeName]
751 schemaBlock := s.getResourceSchemaBlock(req.TypeName)
753 priorStateVal, err := msgpack.Unmarshal(req.PriorState.Msgpack, schemaBlock.ImpliedType())
755 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
759 plannedStateVal, err := msgpack.Unmarshal(req.PlannedState.Msgpack, schemaBlock.ImpliedType())
761 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
765 info := &terraform.InstanceInfo{
769 priorState, err := res.ShimInstanceStateFromValue(priorStateVal)
771 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
775 private := make(map[string]interface{})
776 if len(req.PlannedPrivate) > 0 {
777 if err := json.Unmarshal(req.PlannedPrivate, &private); err != nil {
778 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
783 var diff *terraform.InstanceDiff
786 // a null state means we are destroying the instance
787 if plannedStateVal.IsNull() {
789 diff = &terraform.InstanceDiff{
790 Attributes: make(map[string]*terraform.ResourceAttrDiff),
791 Meta: make(map[string]interface{}),
795 diff, err = schema.DiffFromValues(priorStateVal, plannedStateVal, stripResourceModifiers(res))
797 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
803 diff = &terraform.InstanceDiff{
804 Attributes: make(map[string]*terraform.ResourceAttrDiff),
805 Meta: make(map[string]interface{}),
809 // add NewExtra Fields that may have been stored in the private data
810 if newExtra := private[newExtraKey]; newExtra != nil {
811 for k, v := range newExtra.(map[string]interface{}) {
812 d := diff.Attributes[k]
815 d = &terraform.ResourceAttrDiff{}
819 diff.Attributes[k] = d
827 for k, d := range diff.Attributes {
828 // We need to turn off any RequiresNew. There could be attributes
829 // without changes in here inserted by helper/schema, but if they have
830 // RequiresNew then the state will be dropped from the ResourceData.
831 d.RequiresNew = false
833 // Check that any "removed" attributes that don't actually exist in the
834 // prior state, or helper/schema will confuse itself
836 if _, ok := priorState.Attributes[k]; !ok {
837 delete(diff.Attributes, k)
842 newInstanceState, err := s.provider.Apply(info, priorState, diff)
843 // we record the error here, but continue processing any returned state.
845 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
847 newStateVal := cty.NullVal(schemaBlock.ImpliedType())
849 // Always return a null value for destroy.
850 // While this is usually indicated by a nil state, check for missing ID or
851 // attributes in the case of a provider failure.
852 if destroy || newInstanceState == nil || newInstanceState.Attributes == nil || newInstanceState.ID == "" {
853 newStateMP, err := msgpack.Marshal(newStateVal, schemaBlock.ImpliedType())
855 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
858 resp.NewState = &proto.DynamicValue{
864 // We keep the null val if we destroyed the resource, otherwise build the
865 // entire object, even if the new state was nil.
866 newStateVal, err = schema.StateValueFromInstanceState(newInstanceState, schemaBlock.ImpliedType())
868 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
872 newStateVal = normalizeNullValues(newStateVal, plannedStateVal, true)
874 newStateVal = copyTimeoutValues(newStateVal, plannedStateVal)
876 newStateMP, err := msgpack.Marshal(newStateVal, schemaBlock.ImpliedType())
878 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
881 resp.NewState = &proto.DynamicValue{
885 meta, err := json.Marshal(newInstanceState.Meta)
887 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
892 // This is a signal to Terraform Core that we're doing the best we can to
893 // shim the legacy type system of the SDK onto the Terraform type system
894 // but we need it to cut us some slack. This setting should not be taken
895 // forward to any new SDK implementations, since setting it prevents us
896 // from catching certain classes of provider bug that can lead to
897 // confusing downstream errors.
898 resp.LegacyTypeSystem = true
903 func (s *GRPCProviderServer) ImportResourceState(_ context.Context, req *proto.ImportResourceState_Request) (*proto.ImportResourceState_Response, error) {
904 resp := &proto.ImportResourceState_Response{}
906 info := &terraform.InstanceInfo{
910 newInstanceStates, err := s.provider.ImportState(info, req.Id)
912 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
916 for _, is := range newInstanceStates {
917 // copy the ID again just to be sure it wasn't missed
918 is.Attributes["id"] = is.ID
920 resourceType := is.Ephemeral.Type
921 if resourceType == "" {
922 resourceType = req.TypeName
925 schemaBlock := s.getResourceSchemaBlock(resourceType)
926 newStateVal, err := hcl2shim.HCL2ValueFromFlatmap(is.Attributes, schemaBlock.ImpliedType())
928 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
932 newStateMP, err := msgpack.Marshal(newStateVal, schemaBlock.ImpliedType())
934 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
938 meta, err := json.Marshal(is.Meta)
940 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
944 importedResource := &proto.ImportResourceState_ImportedResource{
945 TypeName: resourceType,
946 State: &proto.DynamicValue{
952 resp.ImportedResources = append(resp.ImportedResources, importedResource)
958 func (s *GRPCProviderServer) ReadDataSource(_ context.Context, req *proto.ReadDataSource_Request) (*proto.ReadDataSource_Response, error) {
959 resp := &proto.ReadDataSource_Response{}
961 schemaBlock := s.getDatasourceSchemaBlock(req.TypeName)
963 configVal, err := msgpack.Unmarshal(req.Config.Msgpack, schemaBlock.ImpliedType())
965 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
969 info := &terraform.InstanceInfo{
973 // Ensure there are no nulls that will cause helper/schema to panic.
974 if err := validateConfigNulls(configVal, nil); err != nil {
975 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
979 config := terraform.NewResourceConfigShimmed(configVal, schemaBlock)
981 // we need to still build the diff separately with the Read method to match
983 diff, err := s.provider.ReadDataDiff(info, config)
985 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
989 // now we can get the new complete data source
990 newInstanceState, err := s.provider.ReadDataApply(info, diff)
992 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
996 newStateVal, err := schema.StateValueFromInstanceState(newInstanceState, schemaBlock.ImpliedType())
998 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
1002 newStateVal = copyTimeoutValues(newStateVal, configVal)
1004 newStateMP, err := msgpack.Marshal(newStateVal, schemaBlock.ImpliedType())
1006 resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err)
1009 resp.State = &proto.DynamicValue{
1010 Msgpack: newStateMP,
1015 func pathToAttributePath(path cty.Path) *proto.AttributePath {
1016 var steps []*proto.AttributePath_Step
1018 for _, step := range path {
1019 switch s := step.(type) {
1020 case cty.GetAttrStep:
1021 steps = append(steps, &proto.AttributePath_Step{
1022 Selector: &proto.AttributePath_Step_AttributeName{
1023 AttributeName: s.Name,
1030 i, _ := s.Key.AsBigFloat().Int64()
1031 steps = append(steps, &proto.AttributePath_Step{
1032 Selector: &proto.AttributePath_Step_ElementKeyInt{
1037 steps = append(steps, &proto.AttributePath_Step{
1038 Selector: &proto.AttributePath_Step_ElementKeyString{
1039 ElementKeyString: s.Key.AsString(),
1046 return &proto.AttributePath{Steps: steps}
1049 // helper/schema throws away timeout values from the config and stores them in
1050 // the Private/Meta fields. we need to copy those values into the planned state
1051 // so that core doesn't see a perpetual diff with the timeout block.
1052 func copyTimeoutValues(to cty.Value, from cty.Value) cty.Value {
1053 // if `to` is null we are planning to remove it altogether.
1057 toAttrs := to.AsValueMap()
1058 // We need to remove the key since the hcl2shims will add a non-null block
1059 // because we can't determine if a single block was null from the flatmapped
1060 // values. This needs to conform to the correct schema for marshaling, so
1061 // change the value to null rather than deleting it from the object map.
1062 timeouts, ok := toAttrs[schema.TimeoutsConfigKey]
1064 toAttrs[schema.TimeoutsConfigKey] = cty.NullVal(timeouts.Type())
1067 // if from is null then there are no timeouts to copy
1069 return cty.ObjectVal(toAttrs)
1072 fromAttrs := from.AsValueMap()
1073 timeouts, ok = fromAttrs[schema.TimeoutsConfigKey]
1075 // timeouts shouldn't be unknown, but don't copy possibly invalid values either
1076 if !ok || timeouts.IsNull() || !timeouts.IsWhollyKnown() {
1077 // no timeouts block to copy
1078 return cty.ObjectVal(toAttrs)
1081 toAttrs[schema.TimeoutsConfigKey] = timeouts
1083 return cty.ObjectVal(toAttrs)
1086 // stripResourceModifiers takes a *schema.Resource and returns a deep copy with all
1087 // StateFuncs and CustomizeDiffs removed. This will be used during apply to
1088 // create a diff from a planned state where the diff modifications have already
1090 func stripResourceModifiers(r *schema.Resource) *schema.Resource {
1094 // start with a shallow copy
1095 newResource := new(schema.Resource)
1098 newResource.CustomizeDiff = nil
1099 newResource.Schema = map[string]*schema.Schema{}
1101 for k, s := range r.Schema {
1102 newResource.Schema[k] = stripSchema(s)
1108 func stripSchema(s *schema.Schema) *schema.Schema {
1112 // start with a shallow copy
1113 newSchema := new(schema.Schema)
1116 newSchema.StateFunc = nil
1118 switch e := newSchema.Elem.(type) {
1119 case *schema.Schema:
1120 newSchema.Elem = stripSchema(e)
1121 case *schema.Resource:
1122 newSchema.Elem = stripResourceModifiers(e)
1128 // Zero values and empty containers may be interchanged by the apply process.
1129 // When there is a discrepency between src and dst value being null or empty,
1130 // prefer the src value. This takes a little more liberty with set types, since
1131 // we can't correlate modified set values. In the case of sets, if the src set
1132 // was wholly known we assume the value was correctly applied and copy that
1133 // entirely to the new value.
1134 // While apply prefers the src value, during plan we prefer dst whenever there
1135 // is an unknown or a set is involved, since the plan can alter the value
1136 // however it sees fit. This however means that a CustomizeDiffFunction may not
1137 // be able to change a null to an empty value or vice versa, but that should be
1138 // very uncommon nor was it reliable before 0.12 either.
1139 func normalizeNullValues(dst, src cty.Value, apply bool) cty.Value {
1141 if !src.IsNull() && !src.IsKnown() {
1142 // Return src during plan to retain unknown interpolated placeholders,
1143 // which could be lost if we're only updating a resource. If this is a
1144 // read scenario, then there shouldn't be any unknowns at all.
1145 if dst.IsNull() && !apply {
1151 // Handle null/empty changes for collections during apply.
1152 // A change between null and empty values prefers src to make sure the state
1153 // is consistent between plan and apply.
1154 if ty.IsCollectionType() && apply {
1155 dstEmpty := !dst.IsNull() && dst.IsKnown() && dst.LengthInt() == 0
1156 srcEmpty := !src.IsNull() && src.IsKnown() && src.LengthInt() == 0
1158 if (src.IsNull() && dstEmpty) || (srcEmpty && dst.IsNull()) {
1163 if src.IsNull() || !src.IsKnown() || !dst.IsKnown() {
1168 case ty.IsMapType(), ty.IsObjectType():
1169 var dstMap map[string]cty.Value
1171 dstMap = dst.AsValueMap()
1174 dstMap = map[string]cty.Value{}
1177 srcMap := src.AsValueMap()
1178 for key, v := range srcMap {
1179 dstVal, ok := dstMap[key]
1180 if !ok && apply && ty.IsMapType() {
1181 // don't transfer old map values to dst during apply
1185 if dstVal == cty.NilVal {
1186 if !apply && ty.IsMapType() {
1187 // let plan shape this map however it wants
1190 dstVal = cty.NullVal(v.Type())
1193 dstMap[key] = normalizeNullValues(dstVal, v, apply)
1196 // you can't call MapVal/ObjectVal with empty maps, but nothing was
1197 // copied in anyway. If the dst is nil, and the src is known, assume the
1199 if len(dstMap) == 0 {
1200 if dst.IsNull() && src.IsWhollyKnown() && apply {
1207 // helper/schema will populate an optional+computed map with
1208 // unknowns which we have to fixup here.
1209 // It would be preferable to simply prevent any known value from
1210 // becoming unknown, but concessions have to be made to retain the
1211 // broken legacy behavior when possible.
1212 for k, srcVal := range srcMap {
1213 if !srcVal.IsNull() && srcVal.IsKnown() {
1214 dstVal, ok := dstMap[k]
1219 if !dstVal.IsNull() && !dstVal.IsKnown() {
1225 return cty.MapVal(dstMap)
1228 return cty.ObjectVal(dstMap)
1230 case ty.IsSetType():
1231 // If the original was wholly known, then we expect that is what the
1232 // provider applied. The apply process loses too much information to
1233 // reliably re-create the set.
1234 if src.IsWhollyKnown() && apply {
1238 case ty.IsListType(), ty.IsTupleType():
1239 // If the dst is null, and the src is known, then we lost an empty value
1240 // so take the original.
1242 if src.IsWhollyKnown() && src.LengthInt() == 0 && apply {
1246 // if dst is null and src only contains unknown values, then we lost
1247 // those during a read or plan.
1248 if !apply && !src.IsNull() {
1250 for _, v := range src.AsValueSlice() {
1264 // if the lengths are identical, then iterate over each element in succession.
1265 srcLen := src.LengthInt()
1266 dstLen := dst.LengthInt()
1267 if srcLen == dstLen && srcLen > 0 {
1268 srcs := src.AsValueSlice()
1269 dsts := dst.AsValueSlice()
1271 for i := 0; i < srcLen; i++ {
1272 dsts[i] = normalizeNullValues(dsts[i], srcs[i], apply)
1275 if ty.IsTupleType() {
1276 return cty.TupleVal(dsts)
1278 return cty.ListVal(dsts)
1281 case ty.IsPrimitiveType():
1282 if dst.IsNull() && src.IsWhollyKnown() && apply {
1290 // validateConfigNulls checks a config value for unsupported nulls before
1291 // attempting to shim the value. While null values can mostly be ignored in the
1292 // configuration, since they're not supported in HCL1, the case where a null
1293 // appears in a list-like attribute (list, set, tuple) will present a nil value
1294 // to helper/schema which can panic. Return an error to the user in this case,
1295 // indicating the attribute with the null value.
1296 func validateConfigNulls(v cty.Value, path cty.Path) []*proto.Diagnostic {
1297 var diags []*proto.Diagnostic
1298 if v.IsNull() || !v.IsKnown() {
1303 case v.Type().IsListType() || v.Type().IsSetType() || v.Type().IsTupleType():
1304 it := v.ElementIterator()
1306 kv, ev := it.Element()
1308 diags = append(diags, &proto.Diagnostic{
1309 Severity: proto.Diagnostic_ERROR,
1310 Summary: "Null value found in list",
1311 Detail: "Null values are not allowed for this attribute value.",
1312 Attribute: convert.PathToAttributePath(append(path, cty.IndexStep{Key: kv})),
1317 d := validateConfigNulls(ev, append(path, cty.IndexStep{Key: kv}))
1318 diags = convert.AppendProtoDiag(diags, d)
1321 case v.Type().IsMapType() || v.Type().IsObjectType():
1322 it := v.ElementIterator()
1324 kv, ev := it.Element()
1325 var step cty.PathStep
1327 case v.Type().IsMapType():
1328 step = cty.IndexStep{Key: kv}
1329 case v.Type().IsObjectType():
1330 step = cty.GetAttrStep{Name: kv.AsString()}
1332 d := validateConfigNulls(ev, append(path, step))
1333 diags = convert.AppendProtoDiag(diags, d)