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