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.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 1, 2026

Warning

Rate limit exceeded

@bitgandtter has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 55 minutes and 54 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cb1a6ea9-fbdb-481a-b4cb-f672a211c92c

📥 Commits

Reviewing files that changed from the base of the PR and between b02fb78 and 0f0bbaa.

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

Walkthrough

EnsureTFState in pkg/terraform/files.go was changed to skip creating a synthetic terraform.tfstate when the supplied external identifier tfID is empty, in addition to existing checks for non-empty state or resource deletion.

Changes

Cohort / File(s) Summary
Terraform State Generation
pkg/terraform/files.go
EnsureTFState now returns early when tfID is empty, avoiding generation of a synthetic terraform.tfstate with an undefined ID. No other state construction or file-writing logic changed.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Suggested reviewers

  • ulucinar
  • sergenyalcin
  • erhancagirici

Thanks — any preference on additional tests or edge cases you want covered for empty tfID behavior?

🚥 Pre-merge checks | ✅ 7
✅ Passed checks (7 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: skipping synthetic Terraform state creation when the external identifier is empty, matching the core fix in the PR.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, clearly explaining the problem, root cause, fix, and validation results.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Configuration Api Breaking Changes ✅ Passed PR modifies pkg/terraform/files.go with early return guard for tfID check; new files in pkg/config/ do not constitute breaking changes to existing Configuration API.
Generated Code Manual Edits ✅ Passed File pkg/terraform/files.go does not match the zz_*.go pattern for generated code and lacks auto-generation markers in its header.
Template Breaking Changes ✅ Passed PR modifies only pkg/terraform/files.go (EnsureTFState function); no changes to pkg/controller/external*.go template files detected.

✏️ 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
Review rate limit: 0/1 reviews remaining, refill in 55 minutes and 54 seconds.

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

@Upbound-CLA
Copy link
Copy Markdown

Upbound-CLA commented Apr 30, 2026

CLA assistant check
All committers have signed the CLA.

@bitgandtter bitgandtter force-pushed the fix-empty-tfid-state branch from e051c1f to b02fb78 Compare April 30, 2026 19:38
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.

Signed-off-by: Yasmany Cubela Medina <yasmanycm@gmail.com>
@bitgandtter bitgandtter force-pushed the fix-empty-tfid-state branch from b02fb78 to 0f0bbaa Compare April 30, 2026 19:40
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/terraform/files.go`:
- Around line 245-248: Add a table-driven unit test for the logic in
pkg/terraform/files.go that contains the guard "if !empty ||
meta.WasDeleted(fp.Resource) || tfID == """: create cases that cover (1) empty
state + not deleted + tfID == "" and assert that no synthetic terraform.tfstate
file is written, and (2) complementary cases where state should be written. In
the test, instantiate a fake FileProcessor/FP (or the same test helper used
elsewhere), set fp.Resource so meta.WasDeleted(fp.Resource) returns false, set
empty=true and tfID="" for the critical case, run the function that evaluates
this guard (the function in files.go that writes synthetic state), and assert on
filesystem or in-memory writer that no file was created; repeat for cases where
a file should be created to lock behavior and prevent regressions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3f2bdd7c-08f9-4d67-bdd9-5ac1a662192b

📥 Commits

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

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

Comment thread pkg/terraform/files.go
Comment on lines +245 to +248
// Skip synthetic state creation when: state already exists, resource is
// being deleted, or there is no external identifier yet (new resource).
// Without an ID, Refresh would fail on providers that require it for Read.
if !empty || meta.WasDeleted(fp.Resource) || tfID == "" {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Please add (or point to) a regression test for the empty tfID guard

Nice fix—this guard is key for IdentifierFromProvider flows. Could you add or reference a table-driven unit test that covers: empty state + not deleted + tfID == "" should not write synthetic terraform.tfstate? That would lock the behavior and prevent future observe/create regressions.

As per coding guidelines: **/*.go: “All Upjet code must be covered by tests… Upjet encourages the use of table driven unit tests.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/terraform/files.go` around lines 245 - 248, Add a table-driven unit test
for the logic in pkg/terraform/files.go that contains the guard "if !empty ||
meta.WasDeleted(fp.Resource) || tfID == """: create cases that cover (1) empty
state + not deleted + tfID == "" and assert that no synthetic terraform.tfstate
file is written, and (2) complementary cases where state should be written. In
the test, instantiate a fake FileProcessor/FP (or the same test helper used
elsewhere), set fp.Resource so meta.WasDeleted(fp.Resource) returns false, set
empty=true and tfID="" for the critical case, run the function that evaluates
this guard (the function in files.go that writes synthetic state), and assert on
filesystem or in-memory writer that no file was created; repeat for cases where
a file should be created to lock behavior and prevent regressions.

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.

2 participants