Skip synthetic TF state creation when external identifier is empty#625
Skip synthetic TF state creation when external identifier is empty#625bitgandtter wants to merge 1 commit intocrossplane:mainfrom
Conversation
When a managed resource has no external-name (tfID is empty), upjet was creating a synthetic TF state with all parameters but no ID. This caused TF Plugin Framework providers to fail on Refresh (Read requires ID) before Create could be attempted. Skip synthetic state creation entirely when tfID is empty. This leaves the state file empty, causing Refresh to return Exists=false and properly triggering the Create flow for new resources. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThe Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
When a managed resource uses IdentifierFromProvider and has no external-name yet (a new resource that needs to be created), EnsureTFState creates a synthetic Terraform state populated with all forProvider parameters but no
valid resource ID. This synthetic state causes Refresh (tofu apply -refresh-only) to attempt a Read against the cloud API using those parameters, which fails on Terraform Plugin Framework providers because the Read function
strictly requires the resource ID to construct the API call.
The result is that Observe returns an error before it can determine that the resource does not exist, so the managed reconciler never reaches the Create path. The resource gets stuck in a permanent observe failed: cannot run
refresh loop.
Root cause: EnsureTFState unconditionally creates a synthetic state when the state file is empty, regardless of whether the resource has an external identifier. For Terraform Plugin SDK providers this was silently tolerated
because their Read functions handle missing IDs gracefully (returning "not found"). Terraform Plugin Framework providers are stricter — they require the ID to build the API request and fail with errors like missing required
_id parameter.
Fix: Add tfID == "" to the early-return guard in EnsureTFState. When there is no external identifier, the state file is left empty. This allows Refresh to return Exists: false, which correctly triggers the Create flow in the
managed reconciler.
Flow before fix (new resource, no external-name):
Flow after fix:
This affects all upjet-based providers that wrap Terraform Plugin Framework providers using IdentifierFromProvider external-name configuration. We encountered this with provider-upjet-cloudflare (wrapping Cloudflare TF
provider v5 which uses Plugin Framework), specifically when creating cloudflare_ruleset and cloudflare_dns_record resources from scratch.
I have:
How has this code been tested
Tested end-to-end with https://github.com/wildbitca/provider-upjet-cloudflare (wrapping cloudflare/cloudflare TF provider v5.18.0, Plugin Framework-based) on a Crossplane v2.2.0 cluster:
The fix was validated using a go.mod replace directive pointing to the fork branch, with 6 Crossplane claims managing 20 rulesets and 61 DNS records across 13 Cloudflare zones — all reaching SYNCED=True, READY=True.