hashicorp / terraform-plugin-framework

A next-generation framework for building Terraform providers.
https://developer.hashicorp.com/terraform/plugin/framework
Mozilla Public License 2.0
284 stars 92 forks source link

PlanModifiers does not work in nested attribute #977

Closed shiyuhang0 closed 2 months ago

shiyuhang0 commented 2 months ago

Module version

github.com/hashicorp/terraform-plugin-framework v1.6.1
go 1.21
github.com/hashicorp/terraform-plugin-sdk/v2 v2.30.0

Relevant provider source code

schema

    resp.Schema = schema.Schema{
        Attributes: map[string]schema.Attribute{
            "id": schema.StringAttribute{
                Required:            true,
            },
            "name": schema.StringAttribute{
                Required:            true,
            },
            "status": schema.SingleNestedAttribute{
                Computed:            true,
                Attributes: map[string]schema.Attribute{
                    "version": schema.StringAttribute{
                        Computed:            true,
                        PlanModifiers: []planmodifier.String{
                            stringplanmodifier.UseStateForUnknown(),
                        },
                    },
                    "cluster_status": schema.StringAttribute{
                        Computed:            true,
                    },
                },
            },
        },
    }

Terraform Configuration Files

Debug Output

Expected Behavior

version attribute uses the state value when generating the plan as it applys the UseStateForUnknown.

Actual Behavior

Change name and execute terraform plan always occur version = "xxx" -> (known after apply).

version does not use the state value. Seems PlanModifiers does not work in the nested attribute.

Steps to Reproduce

References

bendbennett commented 2 months ago

Hi @shiyuhang0 👋

Sorry you ran into trouble here. The issue you describe has some overlap with Defaults don't work in nested attributes. Briefly, the UseStateForUnknown() plan modifier declared on the version attribute is only invoked when the value of the object that contains this attribute (i.e., status) is known (i.e., not null or unknown). As no configuration is being supplied for status, as it's computed, the value of this attribute will be unknown.

For instances in which there is a single attribute within the SingleNestedAttribute, one option would be to declare an object-level UseStateForUnknown() plan modifier. If the status object only contained the version attribute, I believe that this would have the desired effect of persisting the version value with the plan showing the previous value from state for version.

            "status": schema.SingleNestedAttribute{
                Computed: true,
                Attributes: map[string]schema.Attribute{
                    "version": schema.StringAttribute{
                        Computed: true,
                    },
                },
                PlanModifiers: []planmodifier.Object{
                    objectplanmodifier.UseStateForUnknown(),
                },
            },

However, the schema you have illustrated contains an additional attribute cluster_status, which I'm assuming needs to be modifiable in the Update method. If this is the case then you can't set an object-level UseStateForUnknown() plan modifier on status, as Terraform will not allow you to mutate the value in the resource Update method if it has already been determined in the plan through usage of the UseStateForUnknown() plan modifier.

One option in cases where you have nested attributes that either require the previous value from state to be persisted during Update operations (e.g., version) or for their value to be mutated (e.g., cluster_status) would be to remove the plan modifier at the attribute level and handle this logic within the Update method. However, this will always result in the plan showing known after apply for both version and cluster_status.

Another option might be to write a plan modifier for the SingleNestedAttribute, status, that uses the value from state for version, but leaves the value for cluster_status as unknown, and requiring setting in the Update() method.

shiyuhang0 commented 2 months ago

@bendbennett Thanks a lot! I don't want to make both version and cluster_status show known after apply. So I wrote a custom plan modifier. I think it is just a workaround, hope the framework will provide a more convenient way.

// clusterResourceStatusModifier implements the plan modifier.
type clusterResourceStatusModifier struct{}

func (m clusterResourceStatusModifier) Description(_ context.Context) string {
    return "The plan modifier for status attribute. It will apply useStateForUnknownModifier to all the nested attributes except the cluster_status attribute."
}

// MarkdownDescription returns a markdown description of the plan modifier.
func (m clusterResourceStatusModifier) MarkdownDescription(_ context.Context) string {
    return "The plan modifier for status attribute. It will apply useStateForUnknownModifier to all the nested attributes except the cluster_status attribute."
}

// PlanModifyObject implements the plan modification logic.
func (m clusterResourceStatusModifier) PlanModifyObject(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) {
    // Do nothing if there is no state value.
    if req.StateValue.IsNull() {
        return
    }

    // Do nothing if there is a known planned value.
    if !req.PlanValue.IsUnknown() {
        return
    }

    // Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up.
    if req.ConfigValue.IsUnknown() {
        return
    }

    // Does not apply to cluster_status attribute
    attributes := req.StateValue.Attributes()
    attributes["cluster_status"] = types.StringUnknown()
    newStateValue, diag := basetypes.NewObjectValue(req.StateValue.AttributeTypes(ctx), attributes)

    resp.Diagnostics.Append(diag...)
    resp.PlanValue = newStateValue
}

func clusterResourceStatus() planmodifier.Object {
    return clusterResourceStatusModifier{}
}
shiyuhang0 commented 2 months ago

I think this issue can be closed if there is no plan to optimize the plugin-framework.

bendbennett commented 2 months ago

Hi @shiyuhang0,

I believe that a bespoke plan modifier at the object level (i.e., on status, the SingleNestedAttribute is the best approach at this time.

github-actions[bot] commented 1 month ago

I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues. If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.