Consider introducing helper methods for marking unconfigured `Computed` attributes as `<unknown>`
Module version
v1.14.1
Background
Briefly, terraform-plugin-framework (Framework) differs from the previous terraform-plugin-sdk/v2 (SDKv2) in how it handles plan modification during PlanResourceChange.
- SDKv2 has a
CustomizeDifffunction that providers can define to influence the plan, and relies soley on Terraform'sproposed_new_statefor it's plan, which often leads to data consistency problems as Terraform always suggests keeping prior state values for computed attributes, rather than marking them as<unknown>so they can be returned during apply. Since SDKv2 does not need to follow data consistency rules, Terraform allows this behavior in isolation, but often causes downstream problems with other providers when an inconsistent data value is returned during apply. - Framework is required by Terraform to follow these data consistency rules exactly, so the planning behavior is different from SDKv2. Framework always marks computed attributes that are unconfigured as
<unknown>if there is a "planned update" proposed by Terraform (determined withprior_stateand the sameproposed_new_statementioned earlier).- Typically, providers can use the
UseStateForUnknownplan modifier to "revert" this marking of<unknown>and get a similar behavior to SDKv2's plan.
- Typically, providers can use the
Problem
One of the issues with this plan approach in Framework is that we detect a planned update before a provider has a chance to run plan modification. The rough order of operations is:
-
PlanResourceChangeis called by Terraform - Framework applies
Defaultvalues for computed attributes toproposed_new_statefrom Terraform - Framework checks if there is a planned updated (i.e. a diff between
prior stateandproposed_new_statew/ defaults - Framework runs provider-defined attribute plan modifiers
- Framework runs provider-defined
ModifyPlan - Framework returns the final plan result to Terraform
If any of the provider-defined logic in step #4 revert the original plan to being a "no-op", all of the Computed attributes will still be marked as unknown. So there will still be a plan, but just of computed values that were marked as unknown because Framework assumed there would be an update. (For most use-cases, this unknown marking is essential, but here it is causing us problems)
If you have a use-case like this, where the plan modification during step #4 might end in a no-op, you can use the UseStateForUnknown plan modifier to achieve that no-op behavior, but there are still cases where you might want a computed attribute to being marked as unknown. In that case you could write a ModifyPlan function like this example: https://github.com/austinvalle/terraform-provider-sandbox/blob/9f43a630d4c3d43b72ea67414ff711548bdc19b9/internal/provider/thing_resource.go#L64-L79
This example has a subtle bug itself and is thus error-prone, since you should check if the value is configured before marking as unknown. You can write a generic function to implement this, but since all underlying Terraform data is written with terraform-plugin-go, it's not easy or familiar for provider developers to use, see: https://github.com/austinvalle/terraform-provider-sandbox/blob/9f43a630d4c3d43b72ea67414ff711548bdc19b9/internal/provider/thing_resource.go#L83-L107
Proposal
Write a helper function that can safely mark a computed attribute as unknown given the correct config/plan data. This function could be placed on the tfsdk.Plan object, or perhaps just a function in a package. The function should ensure that it doesn't mark attributes that cannot be unknown, either due to the config having a value, or the schema not marking the attribute as Computed.
It might also be helpful to accept multiple attribute paths? Or having a "mark all computed" as unknown. The implementation should be similar to internal/fwserver/MarkComputedNilsAsUnknown, with the added flexibility of targeting a single attribute. It's unclear if it'd be useful to recognize default values here, since the usage should likely always be in the ModifyPlan function.
It's been a little bit since I opened this GH issue, but there isn't any active development on this as we're not sure exactly what the design of the helper should be and we don't have any active use-cases pushing this work.
One way to help here is If you're running into a problem that you believe is related to this issue, please share the use-case here and we can start to figure out what a useful helper might look like to help address this 👍🏻
I'm running into a somewhat related issue where this could be useful, but from the opposite side.
The behavior I'm running into is that if there are no planned changes until step 4 produced by an attribute plan modifier, Framework doesn't go back and mark all of the computed attributes as unknown (which it passed on doing in step 3), and thus carries on using their previous state values as known values. This results in Provider produced inconsistent result errors if any of those computed attributes come back with different values from the API post-apply. Having to manually set these computed attributes to <unknown> in a resource plan modifier is painful, since we need to keep the plan modifier in sync with our resource schema as new computed attributes are added.
To more concretely describe our use case, we have a resource that has a pair of attributes:
- An optional string attribute
directorythat contains a path to a directory of files on disk to be uploaded - A computed string attribute
asset_manifest_sha256which tracks any content changes to those files on disk. The value of this attribute is recomputed during planning using an attribute plan modifier, and is intended to trigger a reupload of the files if any changes have occurred.
If the only change to the resource is from content changes to the files on disk, Framework returns the following plan:
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:
# cloudflare_workers_script.workers_script will be updated in-place
~ resource "cloudflare_workers_script" "workers_script" {
~ assets = {
~ asset_manifest_sha256 = "f28b2736f6de69bee4ba03d1a388fe54d06bcc54f485a81f580e5f8a38cc60f3" -> "dc7a7fe6de47611a93a5219ca484666b307e5214f68c44eda78df8ec210b5d57"
# (3 unchanged attributes hidden)
}
id = "tf-assets-script"
# (17 unchanged attributes hidden)
}
Plan: 0 to add, 1 to change, 0 to destroy.
Notably, none of the computed attributes are unknown in the plan and will result in errors after apply, e.g. modified_on will always have changed.
Some way of getting Framework to mark all computed attributes as <unknown> after the attribute plan modifiers determined there should be a planned change would be very helpful.