Skip to content

Skip synthetic TF state creation when external identifier is empty#625

Open
bitgandtter wants to merge 1 commit intocrossplane:mainfrom
wildbitca:fix-empty-tfid-state
Open

Skip synthetic TF state creation when external identifier is empty#625
bitgandtter wants to merge 1 commit intocrossplane:mainfrom
wildbitca:fix-empty-tfid-state

Conversation

@bitgandtter
Copy link
Copy Markdown

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):

  1. EnsureTFState("") → creates synthetic state with parameters, no ID
  2. Refresh → TF provider attempts Read with no ID → fails
  3. Observe returns error → Create never called
  4. Resource stuck in error loop

Flow after fix:

  1. EnsureTFState("") → tfID == "" → returns nil, state stays empty
  2. Refresh → empty state → Exists: false
  3. Observe returns ResourceExists: false → Create is called
  4. Resource created successfully

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:

  • Before fix: cloudflare_ruleset resources without external-name failed with observe failed: cannot run refresh: refresh failed: missing required ruleset_id parameter. Resources never created.
  • After fix: Same resources reach Create flow, are successfully created in Cloudflare, and reconcile to SYNCED=True, READY=True.
  • Existing resources with external-name: No regression — resources with crossplane.io/external-name set continue to be imported and observed correctly via the existing Refresh path (the tfID == "" guard is not triggered).
  • Deletion: No regression — meta.WasDeleted guard remains unchanged.

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.

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>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4782fe71-3d4b-4514-a5a8-9000d5c4c6dc

📥 Commits

Reviewing files that changed from the base of the PR and between 52796ea and e051c1f.

📒 Files selected for processing (1)
  • pkg/terraform/files.go

📝 Walkthrough

Walkthrough

The EnsureTFState function in pkg/terraform/files.go was modified to add an additional condition for skipping synthetic Terraform state file creation. It now skips when tfID is empty, in addition to the existing checks for non-empty state or resources marked for deletion.

Changes

Cohort / File(s) Summary
Terraform State Generation
pkg/terraform/files.go
Modified EnsureTFState to skip creating synthetic terraform.tfstate when tfID is empty, preventing state generation with an undefined ID.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately describes the main change: adding a guard to skip synthetic state creation when tfID is empty, which is the core fix in this changeset.
Description check ✅ Passed The description comprehensively explains the bug, root cause, the fix, and validation testing—all directly related to the changeset's purpose of preventing synthetic state creation with empty tfID.
Configuration Api Breaking Changes ✅ Passed No files in pkg/config/ were modified; all changes are confined to pkg/terraform/files.go with no function signature or public API alterations.
Generated Code Manual Edits ✅ Passed File pkg/terraform/files.go does not match zz_*.go pattern for generated code, so manual edits are appropriate.
Template Breaking Changes ✅ Passed Check not applicable: PR only modified pkg/terraform/files.go (utility module), not pkg/controller/external*.go template files. Change is scoped to state file handling and does not affect generated providers.

✏️ 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant