terraform-plugin-framework icon indicating copy to clipboard operation
terraform-plugin-framework copied to clipboard

Errantly Reflecting a List Into a Struct Provides Incorrect Error Details

Open bflad opened this issue 2 years ago • 5 comments

Module version

v1.2.0

Relevant provider source code

The original issue occurred in a provider schema, although this is issue should theoretically be reproducible in data sources and resources as well.

Schema:

schema.Schema{
  Blocks: map[string]schema.Block{
    "list_block": schema.ListNestedBlock{
      NestedObject: schema.NestedBlockObject{
        Attributes: map[string]schema.Attribute{/* ... */},
      },
    },
  },
}

Models:

type ProviderModel struct {
  ListBlock types.List `tfsdk:"list_block"`
}

type ListBlockModel struct {
  // ...
}

Logic:

func (p ExampleCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
  var data ProviderModel

  resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)

  if resp.Diagnostics.HasError() {
    return
  }

  // this should be []ListBlockModel below
  var listBlocks ListBlockModel

  // omitting null/unknown checking for example brevity
  // error is raised here if list has elements
  resp.Diagnostics.Append(data.ListBlock.ElementsAs(ctx, &listBlocks, false)...)

  if resp.Diagnostics.HasError() {
    return
  }

  // ...
}

Terraform Configuration Files

list_block {
  # ...
}

Expected Behavior

The error message should indicate that it is necessary to reflect a list into a slice.

cannot reflect tftypes.List[tftypes.Object[...]] into a struct, must be a slice

Actual Behavior

The error message indicates that it is necessary to reflect a list into an object.

Error: Value Conversion Error

  {CONFIG SNIPPET}

An unexpected error was encountered trying to convert tftypes.Value into
provider.ProviderModel. This is always an error in the provider. Please
report the following to the provider developer:

cannot reflect tftypes.List[tftypes.Object[...]] into a struct, must be an object

It is likely this occurs with maps and sets as well, although I haven't personally verified.

Steps to Reproduce

  1. go test -count=1 -v ./internal/provider with a covering acceptance test

bflad avatar Apr 13 '23 16:04 bflad

@bflad any update on this issue?

thulasirajkomminar avatar Feb 09 '24 22:02 thulasirajkomminar

Hey there @thulasirajkomminar 👋🏻 ,

No major updates on this error message. The internal reflection logic causing this errant message is based on the input (in this case, a struct, which always attempts to utilize an object). It's possible we could reword the error message as a whole to reduce confusion but we're not looking into the reflection logic for this at the moment.

We have recently revamped our type and attribute documentation to add more specific detail about what types are expected for each attribute and how they can be used:

  • https://developer.hashicorp.com/terraform/plugin/framework/handling-data/blocks/list-nested
  • https://developer.hashicorp.com/terraform/plugin/framework/handling-data/types/list

austinvalle avatar Feb 09 '24 22:02 austinvalle

@austinvalle So correct me if im wrong.. If im implementing new schema then i just have to use attributes like below. I should not be using blocks.

resource "influxdb_authorization" "sample" {
  description = "This is a sample authorization"

  permissions = [{
    action = "read"
    resource = {
      id   = influxdb_bucket.authorization.id
      type = "buckets"
    }
    },
    {
      action = "write"
      resource = {
        id   = influxdb_bucket.authorization.id
        type = "buckets"
      }
  }]
}

thulasirajkomminar avatar Feb 09 '24 23:02 thulasirajkomminar

Ah, there may be some confusion on the purpose of this issue. The first configuration you shared is definitely valid and possible in framework. The bug report was related to the error message when attempting to perform invalid reflection.

If you have more general purpose plugin framework development questions, please ask them on our discuss board.

Just a quick answer, for the first config you provided, it could be modeled with blocks like this:

schema.Schema{
  Blocks: map[string]schema.Block{
    "permissions": schema.ListNestedBlock{
      NestedObject: schema.NestedBlockObject{
        Attributes: map[string]schema.Attribute{
          "action": schema.StringAttribute{
            Required: true,
          },
        },
        Blocks: map[string]schema.Block{
          "resource": schema.SingleNestedBlock{
            Attributes: map[string]schema.Attribute{
              "id": schema.StringAttribute{
                Required: true,
              },
              "type": schema.StringAttribute{
                Required: true,
              },
            },
          },
        },
      },
    },
  },
}
type AuthorizationResourceModel struct {
	Permissions types.List `tfsdk:"permissions"`
}

type PermissionModel struct {
	Action   types.String `tfsdk:"action"`
	Resource types.Object `tfsdk:"resource"`
}

type ResourceModel struct {
	Id   types.String `tfsdk:"id"`
	Type types.String `tfsdk:"type"`
}

And to answer your second question, if you're supporting Terraform versions 1.0+, then we recommend you utilize Nested Attributes, like your second config.

austinvalle avatar Feb 09 '24 23:02 austinvalle

Thanks its clear now.

thulasirajkomminar avatar Feb 09 '24 23:02 thulasirajkomminar