crossplane / crossplane/upjet

TPF RequiresReplace firing on null forProvider because plan runs before LateInitialization

Open
#725 0 comments 0 reactions 0 assignees View on GitHub
bug
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.