Access Resource Configuration in Plugin Framework Read

Hey there!

I’m working on a Terraform Provider for a resource, using the “new” Plugin Framework. In the Read implementation for that resource, I need to get access to the Resource Configuration e.g. the state in the .tf file, declaring that resource.

Usually, this wouldn’t be an issue, as the state should align with the values in that file, however this is not the case when the Read step is performed after a resource import. Sadly, I am not able to fetch the values in question from the actual resource’s API (which I know is likely bad practice), because I can only fetch parts of that data from the remote resource. With the state in the configuration, I would be able to build the correct Terraform State for the imported resource.

Because I haven’t been able to get to that configuration, I tried a workaround using a Plan Modifier, which would be run on the first apply after the state has been imported. Here I ran into another issue, where I could not access values that should be set on the resource struct. Example:

type resource struct {
  client *someClient
}

func (r *resource) Schema(
  _ context.Context,
  _ resource.SchemaRequest, 
  resp *resource.SchemaResponse
) {
  if (r.client == nil) {
    panic("Oh no!") // This happens when I run `terraform plan`
  }
  
  // ...
}

In the plan modifier I do need access to the “client” above, in order to fetch other parts of the data, which can then be applied to the plan. I’m not sure if that behavior is intended or if I’m doing something wrong. It would make senese to me that the Configure function setting up the client has not been called yet when Schema is called, however I would expect it to be present when the plan modifier is called…

I’d much appreciate any input regarding either of my issues :​)

Best,
Michael

Sadly, Terraform refuses to allow this:

despite it affecting many provider authors trying to do exactly what you are doing.

Heya Max,

Thanks for the quick response! That’s a bummer… I was hoping there would be some way to wrok around that… I’ll probably just end up having to manually do an apply after the import, to fix up the state.

Cheers!

Hi @StrawbFrap :wave:

I believe you should be able to use Resource Plan Modification for the case you describe.

You can call the Configure Method on the provider to set-up your client.

func (p *exampleProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
	// Perform whatever set-up is necessary and assign the configured client to `ResourceData` in the response.
	resp.ResourceData = &http.Client{}
}

In the resource, you should then have access to the client in the Configure Method.

var _ resource.ResourceWithConfigure = (*exampleResource)(nil)

func (e *exampleResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
	if req.ProviderData == nil {
		return
	}

	client, ok := req.ProviderData.(*http.Client)

	if !ok {
		resp.Diagnostics.AddError(
			"Unexpected Resource Configure Type",
			fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
		)

		return
	}

	e.client = client
}

The configured client should then be available in the Modify Plan method which receives the config, state and plan in the resource.ModifyPlanRequest.

var _ resource.ResourceWithModifyPlan = (*exampleResource)(nil)

func (e *exampleResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
	// Do something with the client 
}

The client should also be available within the CRUD functions too.

func (e *exampleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
	if e.client == nil {
		resp.Diagnostics.AddError(
			"Unconfigured HTTP Client",
			"Expected configured HTTP client. Please report this issue to the provider developers.",
		)
	}

	// do something with client

	/* ... */
}

func (e *exampleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
	if e.client == nil {
		resp.Diagnostics.AddError(
			"Unconfigured HTTP Client",
			"Expected configured HTTP client. Please report this issue to the provider developers.",
		)
	}

	// do something with client
	
	/* ... */
}

It may help with implementation to understand the reasoning going on here. In order for Terraform to be able to rely on the ReadResource call to operate consistently, it cannot depend on any configuration data, because configuration data may not (and often does not) correspond to the current state of a resource. The configuration declares the desired state of a resource, while the payload of the ReadResource call contains the last known state. In other words, ReadResource can’t differentiate between the act of importing and a normal refresh.

This is why many resources combine all necessary import configuration into the import ID string, rather than just rely on whatever single opaque identifier the provider might issue for the resource. While not the final solution, the next release will allow interpolation of the ID string which can facilitate the insertion of multiple configuration values during import.

We acknowledge there is a need for more (structured) configuration to be present in the ImportResourceState call, and the first step is being done to devise a way for providers to declare the schema for what type of data is required for import. This will make import configuration easier removing the need for bespoke serialization formats of the import ID, allowing for easier interpolation of complex import data, and make it possible to develop new ideas like resource discovery.

1 Like

Hey @bendbennett ,

Thank you for the detailed response! I didn’t know about the ModifyPlanRequest method before, which will definitely come in handy in the future :​) However, this still doesn’t seem to fix my issue entirely, as I also need to be able to remove a resource, whose full identifier I can only derive from the configuration. In order to delete the resource, I would need to add it to the state, and then omit it in the plan. At least that’s my understanding of how terraform works.

I highly appreciate the background info, @jbardin. I ended up requireing the necessary configuration data be provided as part of the ID. This makes manually building an import command for the resource a little unpractical, but fixes all the issues we’ve had to deal with before and much simplifies the overall logic. We don’t need to do any manual resource imports, which makes this rather a non-issue, still I’m happy to hear the terraform team is working on improving this experience! :​P