-
Notifications
You must be signed in to change notification settings - Fork 98
Read-only computed attribute appears in every plan when adjacent to an optional+computed SetNestedAttribute or ListNestedAttribute with no default #898
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Hi @henryrecker-pingidentity 👋 Thank you for reporting this and sorry you are running into trouble here. Much appreciated for the clear reproduction case as I was able to observe the same behavior. Enabling
Using $ fq -d msgpack torepr 1703789678317_PlanResourceChange_Request_PriorState.msgpack
{
"readonly": "examplereadonly",
"set": [
{
"name": "examplename"
}
]
}
$ fq -d msgpack torepr 1703789678317_PlanResourceChange_Request_ProposedNewState.msgpack
{
"readonly": "examplereadonly",
"set": null
}
$ fq -d msgpack torepr 1703789678317_PlanResourceChange_Response_PlannedState.msgpack
{
"readonly": "\u0000", // torepr representation of unknown
"set": [
{
"name": "examplename"
}
]
} This matches the human-readable plan output and all of these behaviors are currently expected in the latest framework implementation details. In particular the "if the plan differs from the current resource state" part of step 2 there, according to the data:
What is interesting and I'll double check is a SetAttribute. It should behave the same from Terraform's perspective in the protocol data, but if you are seeing differently and I can reproduce, I have a hunch that this is something that needs to be fixed in both Terraform and the framework. More on that after I try out SetAttribute though and confirm that hunch. |
Okay, hunch confirmed, as suspected Terraform's data handling behavior is indeed different: "set": schema.SetAttribute{
Optional: true,
Computed: true,
// no default set in schema
PlanModifiers: []planmodifier.Set{
setplanmodifier.UseStateForUnknown(),
},
ElementType: types.StringType,
}, $ fq -d msgpack torepr 1703791686878_PlanResourceChange_Request_PriorState.msgpack
{
"readonly": "examplereadonly",
"set": [
"examplename"
]
}
$ fq -d msgpack torepr 1703791686878_PlanResourceChange_Request_ProposedNewState.msgpack
{
"readonly": "examplereadonly",
"set": [ // not null!
"examplename"
]
}
$ fq -d msgpack torepr 1703791686878_PlanResourceChange_Response_PlannedState.msgpack
{
"readonly": "examplereadonly",
"set": [
"examplename"
]
} Presumably, Terraform should behave the same whether its an attribute or a nested attribute. That feels like a Terraform bug, which I will submit if there is not already one. This leaves us in an awkward position from the framework though. If/when that change occurs in Terraform, it will not apply to environments running an earlier version of Terraform. Presumably we need to catch the situation as it occurs today, to prevent the plan from showing unexpected changes. Back in #783 (comment) I had mentioned:
It is probably time to actually write up that issue and prioritize it, but it is going to be quite difficult to implement, unfortunately. |
Thanks @bflad . For now, I'm working around this issue by putting UseStateForUnknown on the read-only attribute to prevent the unnecessary plans, and then checking for other "real" changes in a ModifyPlan. If there are other changes, I mark the read-only attribute as unknown manually. The manual checking for other real changes can be somewhat painful if there are multiple readonly attributes like this. |
@henryrecker-pingidentity I did some spelunking in the Terraform core code that handles this and found that it is detecting the Changing the SetNestedAttribute to the following causes an empty plan after apply:
Since it was previously "set": schema.SetNestedAttribute{
Optional: true,
Computed: true,
// no default set in schema
PlanModifiers: []planmodifier.Set{
setplanmodifier.UseStateForUnknown(),
},
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
// Required: true,
Optional: true,
Computed: true,
Validators: []validator.String{
StringNotNull(),
},
},
},
},
}, Where that validator looks something like the below (sorry, this is not something immediately available in package provider
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
)
var _ validator.String = StringNotNullValidator{}
// StringNotNullValidator validates that an attribute is not null. Most
// attributes should set Required: true instead, however in certain scenarios,
// such as a computed nested attribute, all underlying attributes must also be
// computed for planning to not show unexpected differences.
type StringNotNullValidator struct{}
// Description describes the validation in plain text formatting.
func (v StringNotNullValidator) Description(_ context.Context) string {
return "value must be configured"
}
// MarkdownDescription describes the validation in Markdown formatting.
func (v StringNotNullValidator) MarkdownDescription(ctx context.Context) string {
return v.Description(ctx)
}
// Validate performs the validation.
func (v StringNotNullValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
if !req.ConfigValue.IsNull() {
return
}
resp.Diagnostics.AddAttributeError(
req.Path,
"Missing Attribute Value",
req.Path.String()+": "+v.Description(ctx),
)
}
// StringNotNull returns an validator which ensures that the string attribute is
// configured. Most attributes should set Required: true instead, however in
// certain scenarios, such as a computed nested attribute, all underlying
// attributes must also be computed for planning to not show unexpected
// differences.
func StringNotNull() validator.String {
return StringNotNullValidator{}
} Which does nothing when the entire attribute is computed, but triggers the following error when an object exists without the attribute: resource "framework_898" "test" {
set = [{}]
}
Maybe that is a less painful workaround for you. |
Thanks @bflad for the in depth analysis! It appears that
I also previously worked around this in the Speakeasy provider generator by generating a plan modifier that effectively checked "if there are no changes in the entire resource's state and plan (excluding Null <=> Unknown), then propagate prior state for this attribute into current plan state", however this had an error case when a defined element was removed from a plan. E.g. the transition:
to:
Would not result in a change. I couldn't work out a way to write a plan modifier that would detect this. This was my attempt However I can confirm that changing |
@ThomasRooney 👋 Thanks so much for confirming! Given the current situation that folks can unknowingly find themselves in, I think our best course of action for now might be to introduce out-of-the-box validators and then consider raising an implementation error in the framework. If you are interested in contributing those validators, I created hashicorp/terraform-plugin-framework-validators#185 and your example implementation there matches the implementation expectations for that Go module, just with some additional unit testing. Aside: I'll also create a separate issue in the terraform-plugin-framework-validators Go module to consider migrating those validators into the framework Go module proper. We had separated out that Go module originally because we were not sure how the ecosystem would evolve with all the new framework validation concepts, since we really wanted developers to use custom types when possible, and wanted to leave the room for separate versioning of the declarative validators. Over a year after the framework v1.0.0 release now though, the validators Go module has been surprisingly stable. I hesitate to say that everything will move over as-is because another option we do have in some of those cases is introducing some of the very common use case validations into the base types themselves, but that sort of discussion can occur in that issue. |
@bflad are there any updates regarding fixing this issue? |
Hi @akinross 👋 Thanks for reaching out -- I'm no longer at HashiCorp, but the maintainer team might be able to provide any status update. |
Hi @bflad, Thanks for the update and all the best outside of HashiCorp. Will wait on reply from the maintainer team. |
Module version
Relevant provider source code
Terraform Configuration Files
Expected Behavior
Read-only computed attribute should not result in a plan if no other attributes are changed.
Actual Behavior
The following plan is always produced on a plan after the initial create.
When I instead change the "set" attribute to be a SetAttribute of strings rather than a SetNestedAttribute, it works as expected, with no plan being produced after the create. I also tried a simple StringAttribute rather than a set and that worked as expected as well. Only SetNestedAttribute and ListNestedAttribute seems to cause this.
I also found that if a default is set in the schema for the set/list, then it stops generating this plan.
Steps to Reproduce
The text was updated successfully, but these errors were encountered: