Using ModifyPlan with a custom provider

Hi all,

I’m writing my first Terraform provider, using the new Framework. I’ve got quite a lot implemented already, but I’m stuck when trying to use ModifyPlan to dynamically modify the expected state.

I have a “rally configuration” resource in a remote API that is automatically updated by the API as part of the resource creation/update. For example, if I try to create/update a rally configuration with the value {"hello": "world"}, then the API will return the value {"hello": "world", "PresetName": "something"}. I want to work out how to model this in my Terraform provider. I am allowing the user to set {"hello": "world"} but Terraform errors when procesing the response from the API as it does not exactly match the configuration value the user provided.

I’ve tried to use ModifyPlan to model this:


func (r presetResource) ModifyPlan(
	ctx context.Context,
	req resource.ModifyPlanRequest,
	resp *resource.ModifyPlanResponse,
) {

	// If the entire plan is null, the resource is planned for destruction.
	if req.Plan.Raw.IsNull() {
		return
	}

	// Set the PresetName inside the rallyConfiguration, so that Terraform knows what
	// to expect as a response from the SDVI Rally API
	rallyConfigurationString := ""
	diags := resp.Plan.GetAttribute(ctx,
		path.Root("rally_configuration"),
		&rallyConfigurationString,
	)
	resp.Diagnostics.Append(diags...)
	if resp.Diagnostics.HasError() {
		return
	}

	rallyConfiguration := map[string]any{}
	err := json.Unmarshal([]byte(rallyConfigurationString), &rallyConfiguration)
	if err != nil {
		resp.Diagnostics.AddError(
			"Error modifying plan",
			"Could not set modify rally configuration: "+err.Error(),
		)
		return
	}

	name := ""
	diags = resp.Plan.GetAttribute(ctx,
		path.Root("name"),
		&name,
	)
	resp.Diagnostics.Append(diags...)
	if resp.Diagnostics.HasError() {
		return
	}

	rallyConfiguration["PresetName"] = name
	rallyConfigurationBytes, err := jettison.Marshal(rallyConfiguration)
	if err != nil {
		resp.Diagnostics.AddError(
			"Error modifying plan",
			"Could not set modify rally configuration: "+err.Error(),
		)
	}
	resp.Plan.SetAttribute(
		ctx,
		path.Root("rally_configuration"),
		string(rallyConfigurationBytes),
	)
	resp.RequiresReplace.Append(path.Root("rally_configuration"))
}

When I try to terraform plan with this code I get an error:

╷
│ Error: Provider produced invalid plan
│ 
│ Provider "sky.local/gap/sdvi-rally" planned an invalid value for rally_preset.minimal_example.rally_configuration: planned value
│ cty.StringVal("{\"InputFileLabel\":\"a-label\",\"OutputStorageName\":\"what-a-lovely-bucket\",\"PresetName\":\"xxxxxxx\",\"ProxyTypeName\":\"sdvi_proxy\"}") does
│ not match config value cty.StringVal("{\"InputFileLabel\":\"a-label\",\"OutputStorageName\":\"what-a-lovely-bucket\",\"ProxyTypeName\":\"sdvi_proxy\"}\n").
│ 
│ This is a bug in the provider, which should be reported in the provider's own issue tracker.

I’m struggling because I want the planned value to be different from the config value!

I’m struggling to find resources about how to use ModifyPlan and/or how to write custom providers with this level of complexity. Can someone help me out please?

Thanks,
Craig

Hi @masterjakul,

This error is the result of Terraform Core checking one of the lifecycle rules for the PlanResourceChange operation.

These rules are in place to help establish a consistent set of behaviors that module authors can rely on across all providers. In this particular case the intended guarantee is that if a module author writes a particular value for an argument in the configuration and then refers to that argument from elsewhere in the module then the result of that reference should be exactly what they wrote, so that they can follow the flow of data between resources.

Of course, not all remote APIs are well suited to being directly mapped onto Terraform’s standard model, and so as a provider developer you’ll need to make some design compromises to model the remote API behavior as best as possible while still preserving Terraform’s own standard behaviors and assumptions.

One possible way to handle this – a pattern used in some other providers with similar design challenges – is to map this one remote API property to two separate attributes in Terraform, where one is used to specify the author’s intent and then the other is used to reflect the final result back to the calling module for use elsewhere.

I’m not sure exactly what names might make sense here but here’s a possible example of what I mean:

  • base_rally_configuration is either required or optional, so it can be set in the configuration.
  • final_rally_configuration is only “computed”, which means it cannot be set in the configuration and so is under full control of the provider logic.

In your ModifyPlan method you can read from base_rally_configuration, perform the same transformations you are currently doing, but then write the result into final_rally_configuration instead.

Module authors who might want to use these values elsewhere in the module can then choose to refer to either one of these attributes depending on whether they want to use the exact value they wrote or to use the modified version instead.

1 Like

Hi @apparentlymart ,

Thank you so much for such a detailed answer, and for sending it so quickly. I really appreciate the clear way you have explained everything.

I’m glad to know that I’m not missing something simple, and that instead I need to change my approach. I’ll do as you have suggested and have 2 properties - one for what the user asks for and one for the modified version.