]> git.immae.eu Git - github/fretlink/terraform-provider-statuscake.git/blob - vendor/github.com/hashicorp/terraform/helper/plugin/grpc_provider.go
Upgrade to 0.12
[github/fretlink/terraform-provider-statuscake.git] / vendor / github.com / hashicorp / terraform / helper / plugin / grpc_provider.go
1 package plugin
2
3 import (
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
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 // 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.
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
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
372 func (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.
392 func (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
441 func (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
452 func (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
478 func (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
541 func (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
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,
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
903 func (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
958 func (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
1015 func 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.
1052 func 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.
1090 func 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
1108 func 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.
1139 func 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.
1296 func 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 }