TPF RequiresReplace firing on null forProvider because plan runs before LateInitialization
- Dominant language
- Go
- Stars
- 481
- Forks
- 131
- Avg merge
- 2d 1h
- Merged PRs (30d)
- 11
Description
### What happened?
For Terraform Plugin Framework resources, when an upstream provider adds a new Computed+Optional field with a default value and a `RequiresReplace()` plan modifier, all existing managed resources that lack that field in their `spec.forProvider` enter an unrecoverable reconciliation error such as (example for `provider-clickhousedbops`):
```
diff contains fields that require resource replacement: AttributeName("current_grants"), AttributeName("grant_option"),
```
The issue seems to be the sequence followed during `Observe()`, where the plan is computed **before** late initialization runs, and the cached plan is never refreshed afterward. Thus, we "see" that there's a diff (i.e. for a field `spec.forProvider.foo: nil` != `status.atProvider.foo: false`) and this is computed as a "needs replacement" in `planRequiresReplace()`.
Even though late init writes the field into the spec on the first reconcile, the plan is computed again on each `Observe` call from a TF state lacking that field (since the update was refused).
#### Workaround
We work around this for `provider-clickhousedbops` by adding a resource initializer that seeds the missing fields in `status.atProvider` with their upstream defaults before the first reconcile, so the TF state already contains them when the plan runs.
This eliminates the null->default diff but must be maintained per-resource and per-field.
### How can we reproduce it?
Allegedly it affects any Upjet managed resource using the Plugin Framework async external client where an upstream provider version:
- Adds a new field with Computed: true, Optional: true, and a default value
- Marks that field with `RequiresReplace()`
- We had resources reconciled in a previous version, before that new field was marked as "requires replace".
---
originally reported in https://github.com/lansweeper-oss/provider-clickhousedbops/issues/81
Contributor guide
Research direction
Start by tracing the resource Observe() sequence and planRequiresReplace() handling described in the report, then reproduce it with an affected managed resource whose new field is null before late initialization. Done means late initialization is reflected in the plan so an existing resource does not enter an unrecoverable RequiresReplace error; verify the reported null-to-default case.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, terraform
- Domain
- infrastructure
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100