Is there a way to avoid produced inconsistent result after apply?

Original PR: Consider skip comparison for a specified attribute between plan and state after the update · Issue #944 · hashicorp/terraform-plugin-framework · GitHub

I am the developer of terraform-provider-tidbcloud. It developes with terraform-plugin-framework.

In my case, there is a resource contains attribute cluster_status which is computed and marked as UseStateForUnknown . When users want to update the resource, the cluster_status will be changed from available to modifying automatically, thus I updated the cluster_status in terraform state. As a result, the state is different from the plan (the cluster_status in state is modifying but the cluster_status in plan is available ). Then Provider produced inconsistent result occurs.

You can find the schema and update logical here.

I open a pull request to set the cluster_status of the state the same as the plan to avoid this error. But I don’t think it is a good idea.

I think computed attributes(such as status) changed after updating other attributes is common. But produced inconsistent result after apply always occurs when we update them to the state in the update interface. It is strange.

My question is: Can terraform-plugin-framework provide something to skip comparison for a specified attribute between plan and state after the update?
Or is there a good solution I missed?

Hi @shiyuhang0 :wave:

Sorry you ran into trouble here. The issue in this case is that you are mutating the value of cluster_status in the Update method, and that mutation results in a disagreement between the state and the plan.

The reason this is happening is because you have the UseStateForUnknown() plan modifier configured on the cluster_status attribute. As a consequence, the plan that is supplied to the Update method in the resource.UpdateRequest contains a known, value (i.e., available), which is derived from the current state value for cluster_status. If you then mutate the value that is to be stored in state for this attribute in the Update method (i.e., change the value stored in state from available to modifying), then the plan and state do not agree and Terraform raises the error that you have described.

The UseStateForUnknown() plan modifier is recommended for the following scenarios:

UseStateForUnknown returns a plan modifier that copies a known prior state value into the planned value. Use this when it is known that an unconfigured value will remain the same after a resource update.

Depending on your use-case, you may be able to simply remove the UseStateForUnknown() plan modifier from the cluster_status attribute as long as you are setting the value within the provider logic (e.g., in the Update() method), which appears to be the case in this instance.

Thanks for your quick reply!

Removing the UseStateForUnknown() will introduce another issue: when executing terraform plan, diff always occurs for cluster_status. Because this attribute will always be known after apply. Here is an example:

You can deep into this issue for more details.

As you can see. There will be problems whether I use UseStateForUnknown() :frowning:

@shiyuhang0 one possibility is that there another plan difference occurring somewhere, but that it is not displayed in the human-readable output from the CLI (i.e., only the cluster_status change is being displayed).

Could I ask you to check the following:

  • Set the logging to trace and see if the framework is reporting a difference and triggering its unknown marking, and whether logging has anything about which attribute(s) caused the behavior to occur? Trace-level logging can be activated by using TF_LOG=TRACE terraform plan
  • Examine the machine-readable/JSON plan output to see if there are any additional plan differences that are not displayed in the CLI? You can use terraform plan -json for this purpose.
  • Another option is to use Terraform 1.8.0-beta1 and see if more information is visible for the planned changes, as this version has some updates which will display certain instances in which there were changes between empty strings (“”) and null in the CLI plan output.

There are a few other considerations in the context of the cluster_status attribute:

  • Whether that attribute needs to be there at all (is it useful to practitioners)?
  • Whether that attribute needs to be changed on update? If not one option would be not to set the API response value for it into state.
  • Whether that attribute should be planned to change to “modifying” to match the API behavior?
  • Whether the resource update should wait for the modifications before returning, which would theoretically allow this attribute to remain “available”.

I tried to reproduce what you are seeing with the following minimal code. You can see that even though the value for cluster_status changes on Create, Read, and Update, there is no “drift” which is one of the reasons for being suspicious that there may be other changes in the plan that are hidden from the CLI output.

I’d be interested to know whether you’re able to reproduce what you are seeing by modifying the following:

var _ resource.Resource = (*playgroundResource)(nil)

type playgroundResource struct {
}

func NewResource() resource.Resource {
	return &playgroundResource{}
}

func (e *playgroundResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
	resp.TypeName = req.ProviderTypeName + "_resource"
}

func (e *playgroundResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
	resp.Schema = schema.Schema{
		Attributes: map[string]schema.Attribute{
			"configurable_attribute": schema.StringAttribute{
				Optional: true,
			},
			"status": schema.SingleNestedAttribute{
				Computed: true,
				PlanModifiers: []planmodifier.Object{
					objectplanmodifier.UseStateForUnknown(),
				},
				Attributes: map[string]schema.Attribute{
					"cluster_status": schema.StringAttribute{
						Computed: true,
					},
				},
			},
		},
	}
}

type clusterResourceData struct {
	ConfigurableAttribute types.String `tfsdk:"configurable_attribute"`
	Status                types.Object `tfsdk:"status"`
}

func (e *playgroundResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
	var data clusterResourceData

	diags := req.Plan.Get(ctx, &data)
	resp.Diagnostics.Append(diags...)

	if resp.Diagnostics.HasError() {
		return
	}

	statusObj, diags := types.ObjectValue(
		map[string]attr.Type{
			"cluster_status": types.StringType,
		},
		map[string]attr.Value{
			"cluster_status": types.StringValue(time.Now().String()),
		},
	)

	resp.Diagnostics.Append(diags...)

	if resp.Diagnostics.HasError() {
		return
	}

	data.Status = statusObj

	diags = resp.State.Set(ctx, &data)
	resp.Diagnostics.Append(diags...)
}

func (e *playgroundResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
	var data clusterResourceData

	diags := req.State.Get(ctx, &data)
	resp.Diagnostics.Append(diags...)

	if resp.Diagnostics.HasError() {
		return
	}

	statusObj, diags := types.ObjectValue(
		map[string]attr.Type{
			"cluster_status": types.StringType,
		},
		map[string]attr.Value{
			"cluster_status": types.StringValue(time.Now().String()),
		},
	)

	resp.Diagnostics.Append(diags...)

	if resp.Diagnostics.HasError() {
		return
	}

	data.Status = statusObj

	diags = resp.State.Set(ctx, &data)
	resp.Diagnostics.Append(diags...)
}

func (e *playgroundResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
	var data clusterResourceData

	diags := req.Plan.Get(ctx, &data)
	resp.Diagnostics.Append(diags...)

	if resp.Diagnostics.HasError() {
		return
	}

	statusObj, diags := types.ObjectValue(
		map[string]attr.Type{
			"cluster_status": types.StringType,
		},
		map[string]attr.Value{
			"cluster_status": types.StringValue(time.Now().String()),
		},
	)

	resp.Diagnostics.Append(diags...)

	if resp.Diagnostics.HasError() {
		return
	}

	data.Status = statusObj

	diags = resp.State.Set(ctx, &data)
	resp.Diagnostics.Append(diags...)
}

func (e *playgroundResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
	var data clusterResourceData

	diags := req.State.Get(ctx, &data)
	resp.Diagnostics.Append(diags...)

	if resp.Diagnostics.HasError() {
		return
	}
}
1 Like

Thanks a lot for your detailed answer!

As you can not reproduce my question, I suspect it may be because of my terraform-plugin-framework version (I used v1.1.1 before which was published a year ago). After bumping it from v1.1.1 to v1.6.1, the plan difference disappeared.

In conclusion, if anyone has a similar problem to me:

  1. Only use UseStateForUnknown when it is known that an unconfigured value will remain the same after a resource update.
  2. Upgrade the terraform-plugin-framework when computed attribute reports plan difference.

Thanks again @bendbennett, you help me a lot.

@bendbennett
I looked through the whole changelog on the release page and tested them on my case. The #669 is the root cause of the different plan for nested computed attributes. You should be able to reproduce before that commit.

Interesting thing, the issue corresponding to this PR was raised by me too (completely forgot :grinning: )

I noticed a discrepancy between the state and plan due to a computed attribute, specifically the “deployment_status,” which changes dynamically based on resource updates. When applying changes, Terraform’s state reflects the previous status, causing inconsistencies and triggering errors during the apply process. I’m curious to hear your experiences and strategies for handling such state drift scenarios effectively.