Skip to content

feat: add resource identity support to TF Plugin Framework integration#623

Open
erikmiller-gusto wants to merge 2 commits intocrossplane:mainfrom
erikmiller-gusto:feat/resource-identity-support
Open

feat: add resource identity support to TF Plugin Framework integration#623
erikmiller-gusto wants to merge 2 commits intocrossplane:mainfrom
erikmiller-gusto:feat/resource-identity-support

Conversation

@erikmiller-gusto
Copy link
Copy Markdown

@erikmiller-gusto erikmiller-gusto commented Mar 27, 2026

Description of your changes

Pass resource identity data through all protov6 request/response cycles (ReadResource, PlanResourceChange, ApplyResourceChange) so that resources declaring an IdentitySchema in terraform-plugin-framework v1.17.0+ receive the required identity data and no longer fail with "Missing Resource Identity After Read".

Fixes crossplane-contrib/provider-upjet-aws#2003

I have:

  • Read and followed Upjet's contribution process.
  • Run make reviewable to ensure this PR is ready for review.
  • Added backport release-x.y labels to auto-backport this PR if necessary.

How has this code been tested

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds persistent propagation of Terraform Plugin Framework resource identity through Read, Plan, and Apply: the external TF plugin framework client stores planned identity, RPC requests include Prior/Current/Planned identity fields, responses update the client and AsyncTracker, and AsyncTracker gains thread-safe identity getters/setters. Tests added.

Changes

Cohort / File(s) Summary
External client identity handling
pkg/controller/external_tfpluginfw.go
Added plannedIdentity *tfprotov6.ResourceIdentityData to the external client. Plan/Read/Apply RPC requests now include Prior/Current/Planned identity fields; Plan/Read/Apply responses capture Planned/New identity and update the operation tracker. Minor struct-literal alignment changes.
AsyncTracker identity storage
pkg/controller/nofork_store.go
Added fwIdentity *tfprotov6.ResourceIdentityData to AsyncTracker plus thread-safe accessors GetFrameworkIdentity() and SetFrameworkIdentity(id). No changes to existing state-reset logic.
Tests & helpers
pkg/controller/external_tfpluginfw_test.go, pkg/controller/nofork_store_test.go
Expanded test configuration and mock server callbacks to capture requests and return identity fields; added newTestIdentityData helper; added tests verifying identity propagation for Observe/Plan/Create/Update/Delete and AsyncTracker identity behavior.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant ExternalClient
    participant AsyncTracker
    participant TFProvider as TF Provider (RPC)

    Client->>ExternalClient: Observe / Plan / Apply (Create/Update/Delete)
    ExternalClient->>AsyncTracker: GetFrameworkIdentity()
    AsyncTracker-->>ExternalClient: current identity (may be nil)

    ExternalClient->>TFProvider: ReadResourceRequest(CurrentIdentity) / PlanResourceChangeRequest(PriorIdentity) / ApplyResourceChangeRequest(PlannedIdentity)
    TFProvider-->>ExternalClient: ReadResourceResponse(NewIdentity) / PlanResourceChangeResponse(PlannedIdentity) / ApplyResourceChangeResponse(NewIdentity)

    ExternalClient->>AsyncTracker: SetFrameworkIdentity(NewIdentity or nil)
    AsyncTracker-->>ExternalClient: ack
    ExternalClient-->>Client: Return observed/plan/apply result
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Would you like me to point to the specific lines where identities are read/set for quicker review? Thanks for the work on this — it clarifies the identity lifecycle.

🚥 Pre-merge checks | ✅ 6 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Template Breaking Changes ⚠️ Warning Changes to external_tfpluginfw.go affect all generated controller behavior through identity propagation logic but lack evidence of multi-provider testing before merge. Test identity propagation changes with 2-3 additional providers beyond AWS to validate no regressions, then document results in PR.
✅ Passed checks (6 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add resource identity support to TF Plugin Framework integration' is descriptive, under 72 characters (70 chars), and directly reflects the main change in the PR.
Description check ✅ Passed The description clearly explains the purpose of passing resource identity data through protov6 request/response cycles to support terraform-plugin-framework v1.17.0+ and fix the 'Missing Resource Identity After Read' error.
Linked Issues check ✅ Passed All code changes directly address the linked issue #2003 by implementing resource identity data propagation through ReadResource, PlanResourceChange, and ApplyResourceChange cycles to restore failed SecurityGroupRule creation.
Out of Scope Changes check ✅ Passed All changes are focused on adding identity support to the TF Plugin Framework integration; no unrelated modifications or technical debt are introduced outside the stated objective.
Configuration Api Breaking Changes ✅ Passed This pull request makes no changes to files under pkg/config/. All modifications are isolated to pkg/controller/ (external_tfpluginfw.go, nofork_store.go, and their test files), which implement the Terraform Plugin Framework integration layer.
Generated Code Manual Edits ✅ Passed The custom check for manual edits to generated code files passed successfully. This PR modifies only manually-maintained source files, none of which match the zz_*.go generated code file pattern.

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

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/controller/external_tfpluginfw_test.go (1)

148-153: ⚠️ Potential issue | 🔴 Critical

Wire the per-test config into the helper—use testConfig.cfg instead of the undefined bare cfg.

At line 152, the helper references cfg, but unlike the similar pattern in external_tfpluginsdk_test.go (which declares cfg as a package-scoped variable), this file doesn't have that declaration. The helper receives the config via the testConfig parameter and already correctly uses testConfig.cfg elsewhere (line 131). This reference should be consistent.

Suggested fix
-		config: cfg,
+		config: testConfig.cfg,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/controller/external_tfpluginfw_test.go` around lines 148 - 153, The
helper constructing the terraformPluginFrameworkExternalClient mistakenly uses
an undefined bare cfg; update the returned struct to use the per-test
configuration passed in via testConfig (i.e., replace cfg with testConfig.cfg)
so the config field is wired from the testConfig parameter when creating the
terraformPluginFrameworkExternalClient (keep other symbols like ts:
terraform.Setup and mockTPFProvider unchanged).
🤖 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/controller/external_tfpluginfw_test.go`:
- Around line 189-191: The test helper is pre-populating
tpfExternal.plannedIdentity which masks a regression in getDiffPlanResponse()
not copying PlanResourceChangeResponse.PlannedIdentity into the client; change
the tests so the mock PlanResourceChangeResponse remains populated but remove
the global pre-set of tpfExternal.plannedIdentity in the shared helper and
instead explicitly set tpfExternal.plannedIdentity only in the
create/update/delete tests that require a preplanned value (leave
TestTPFPlanIdentityPropagation to rely on Observe() to populate
plannedIdentity), locating the change around the shared helper that constructs
the mock response (getDiffPlanResponse /
PlanResourceChangeResponse.PlannedIdentity) and places
tpfExternal.plannedIdentity before Observe().

In `@pkg/controller/external_tfpluginfw.go`:
- Around line 529-536: In the branch that handles the “empty-state / not found”
result (the code path that calls TerraformPluginFrameworkIsStateEmptyFn and
currently clears fwState), also clear the stored framework identity by calling
n.opTracker.SetFrameworkIdentity(nil); specifically, add this call alongside the
existing n.opTracker.SetFrameworkTFState(nil) (or wherever readResponse.NewState
is treated as empty) so the tracker does not retain readResponse.NewIdentity for
resources classified as gone.

In `@pkg/controller/nofork_store.go`:
- Around line 58-59: AsyncTracker currently holds fwIdentity only in memory, so
after a restart or leader handoff fwIdentity becomes nil while fwState is
reconstructed; update the code that persists/restores fwState to also persist
and reconstruct fwIdentity (e.g., serialize ResourceIdentityData when saving
AsyncTracker state and rehydrate fwIdentity during tracker/load path), or add a
rebuild path in the controller that derives fwIdentity from the persisted
fwState before Read/Plan/Apply; locate and modify AsyncTracker, the fwState
persistence/load routines, and the places that call Read/Plan/Apply to ensure
fwIdentity is available after restart.

---

Outside diff comments:
In `@pkg/controller/external_tfpluginfw_test.go`:
- Around line 148-153: The helper constructing the
terraformPluginFrameworkExternalClient mistakenly uses an undefined bare cfg;
update the returned struct to use the per-test configuration passed in via
testConfig (i.e., replace cfg with testConfig.cfg) so the config field is wired
from the testConfig parameter when creating the
terraformPluginFrameworkExternalClient (keep other symbols like ts:
terraform.Setup and mockTPFProvider unchanged).
🪄 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: 0048bdd7-67a2-4d67-88cb-dc0fea832aa5

📥 Commits

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

📒 Files selected for processing (4)
  • pkg/controller/external_tfpluginfw.go
  • pkg/controller/external_tfpluginfw_test.go
  • pkg/controller/nofork_store.go
  • pkg/controller/nofork_store_test.go

@erikmiller-gusto erikmiller-gusto force-pushed the feat/resource-identity-support branch 3 times, most recently from 988dd3d to f50497d Compare March 27, 2026 21:51
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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/controller/external_tfpluginfw.go (1)

853-870: ⚠️ Potential issue | 🟡 Minor

Identity propagation in delete operation is correct and well-tested.

The code properly stores applyResponse.NewIdentity (nil after successful delete) in the tracker, ensuring deleted resources don't retain identity. Unit tests confirm this behavior via TestTPFDeleteIdentityPropagation.

Note: While this file (pkg/controller/external_tfpluginfw.go) is runtime code rather than a template, the identity propagation touches core resource lifecycle logic. Since the change affects how identity flows through all Terraform execution modes (CLI, SDK v2, Framework), it's worth confirming this has been validated beyond unit tests—especially given the fix targets a real provider issue (provider-upjet-aws#2003).

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

In `@pkg/controller/external_tfpluginfw.go` around lines 853 - 870, The delete
path already records applyResponse.NewIdentity via
n.opTracker.SetFrameworkIdentity after calling n.server.ApplyResourceChange with
an ApplyResourceChangeRequest, but you must ensure this identity-nil propagation
is validated beyond unit tests: confirm the runtime retains the
SetFrameworkIdentity call in pkg/controller/external_tfpluginfw.go (and that
ApplyResourceChangeRequest/ApplyResourceChange usage is unchanged), and
add/execute an integration or e2e test that exercises the delete flow across
CLI, SDK v2 and Framework modes (in addition to
TestTPFDeleteIdentityPropagation) to verify deleted resources' identities become
nil in the opTracker at runtime.
🧹 Nitpick comments (1)
pkg/controller/external_tfpluginfw_test.go (1)

535-552: Helpful test helper!

The newTestIdentityData function creates realistic identity data for tests. Quick question: is this helper also used in pkg/controller/nofork_store_test.go as mentioned in the summary? If so, could this be extracted to a shared test utilities file to avoid duplication?

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

In `@pkg/controller/external_tfpluginfw_test.go` around lines 535 - 552, The
helper newTestIdentityData is duplicated across tests; move it into a shared
test utility (e.g., pkg/controller/testutil or pkg/testutils) and update callers
(pkg/controller/external_tfpluginfw_test.go and
pkg/controller/nofork_store_test.go) to import and call the single shared
function; ensure the function signature and imports (tfprotov6, tftypes) are
preserved and exported/unexported as needed (e.g., NewTestIdentityData) so both
test packages can reference it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@pkg/controller/external_tfpluginfw.go`:
- Around line 853-870: The delete path already records applyResponse.NewIdentity
via n.opTracker.SetFrameworkIdentity after calling n.server.ApplyResourceChange
with an ApplyResourceChangeRequest, but you must ensure this identity-nil
propagation is validated beyond unit tests: confirm the runtime retains the
SetFrameworkIdentity call in pkg/controller/external_tfpluginfw.go (and that
ApplyResourceChangeRequest/ApplyResourceChange usage is unchanged), and
add/execute an integration or e2e test that exercises the delete flow across
CLI, SDK v2 and Framework modes (in addition to
TestTPFDeleteIdentityPropagation) to verify deleted resources' identities become
nil in the opTracker at runtime.

---

Nitpick comments:
In `@pkg/controller/external_tfpluginfw_test.go`:
- Around line 535-552: The helper newTestIdentityData is duplicated across
tests; move it into a shared test utility (e.g., pkg/controller/testutil or
pkg/testutils) and update callers (pkg/controller/external_tfpluginfw_test.go
and pkg/controller/nofork_store_test.go) to import and call the single shared
function; ensure the function signature and imports (tfprotov6, tftypes) are
preserved and exported/unexported as needed (e.g., NewTestIdentityData) so both
test packages can reference it.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cdc689dc-544b-43f8-be54-9527d3c75033

📥 Commits

Reviewing files that changed from the base of the PR and between 988dd3d and f50497d.

📒 Files selected for processing (4)
  • pkg/controller/external_tfpluginfw.go
  • pkg/controller/external_tfpluginfw_test.go
  • pkg/controller/nofork_store.go
  • pkg/controller/nofork_store_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/controller/nofork_store.go
  • pkg/controller/nofork_store_test.go

@erikmiller-gusto erikmiller-gusto force-pushed the feat/resource-identity-support branch from f50497d to 83c4b55 Compare March 30, 2026 04:39
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/controller/external_tfpluginfw_test.go`:
- Around line 816-864: Add a second subtest inside
TestTPFPlanIdentityPropagation that simulates a restart rehydration: configure
the testConfiguration so readNewIdentity is the non-nil identity returned by
ReadResourceResponse but the opTracker starts with nil (or ensure
ReadResourceRequest.CurrentIdentity is nil), then call tpfExternal.Observe(...)
and assert the capturedPlanReq (PlanResourceChangeRequest) has PriorIdentity
equal to the refreshed identity from ReadResourceResponse (the test should still
use capturedPlanReq, tpfExternal.plannedIdentity, readNewIdentity,
planPlannedIdentity, and opTracker to locate the behavior under
Observe/ReadResource/PlanResourceChange flows).
🪄 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: 7d4f562d-bb6d-4f72-b442-d30867656769

📥 Commits

Reviewing files that changed from the base of the PR and between f50497d and 83c4b55.

📒 Files selected for processing (4)
  • pkg/controller/external_tfpluginfw.go
  • pkg/controller/external_tfpluginfw_test.go
  • pkg/controller/nofork_store.go
  • pkg/controller/nofork_store_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/controller/nofork_store_test.go
  • pkg/controller/external_tfpluginfw.go

Pass resource identity data through all protov6 request/response cycles
(ReadResource, PlanResourceChange, ApplyResourceChange) so that
resources declaring an IdentitySchema in terraform-plugin-framework
v1.17.0+ receive the required identity data and no longer fail with
"Missing Resource Identity After Read".

Fixes crossplane-contrib/provider-upjet-aws#2003

Signed-off-by: Erik Miller <erik.miller@gusto.com>
@erikmiller-gusto erikmiller-gusto force-pushed the feat/resource-identity-support branch from 83c4b55 to 7d2845c Compare March 30, 2026 05:04
@erikmiller-gusto
Copy link
Copy Markdown
Author

Have also confirmed that this works and fixed the same issue in the provider-upjet-cloudflare

aditmeno added a commit to canary-technologies-corp/upjet that referenced this pull request Apr 3, 2026
When a resource with an IdentitySchema is deleted externally, the
terraform-plugin-framework's ReadResource returns a null state. The
AWS provider's identity interceptor skips populating identity for null
states, but the framework still validates that identity must be non-null
after a successful Read - producing the "Missing Resource Identity After
Read" error.

This change treats that specific diagnostic as a resource-not-found
condition, allowing Upjet to correctly detect that the external resource
no longer exists and proceed with recreation.

Complements the identity propagation from PR crossplane#623.
@aditmeno
Copy link
Copy Markdown

aditmeno commented Apr 3, 2026

Thanks for the PR — the identity propagation fix is necessary but not sufficient on its own. We tested it in production and hit the same Missing Resource Identity After Read error on aws_s3_bucket_lifecycle_configuration (and likely any resource with an IdentitySchema that gets deleted externally or doesn't yet exist).

Root Cause

The PR's comment in nofork_store.go assumes:

after a restart, the first Observe sends nil CurrentIdentity, and the provider's identity interceptor populates NewIdentity from state/client data in the ReadResourceResponse, rehydrating the tracker.

This is only true when the resource still exists. When the resource has been deleted externally (or hasn't been created yet):

  1. The provider's Read sets response.State to null (resource gone)
  2. The AWS provider's identity interceptor (identity_interceptor.go) checks response.State.Raw.IsNull() and skips populating identity
  3. The terraform-plugin-framework server (server_readresource.go L201-209) validates that identity must be non-null after a successful Read — and returns the Missing Resource Identity After Read error diagnostic

Additional Fix Needed

In Observe(), the Missing Resource Identity After Read diagnostic should be treated as a resource-not-found condition (similar to how isResourceNotFoundDiags already works). Here's the change we applied on top of this PR:

// hasMissingResourceIdentityDiagnostic checks whether the diagnostics contain
// the "Missing Resource Identity After Read" error that terraform-plugin-framework
// returns when a resource with an IdentitySchema has its state set to null
// (resource deleted externally) but the identity interceptor skips populating
// identity for null states. We treat this as a resource-not-found condition.
func hasMissingResourceIdentityDiagnostic(diags []*tfprotov6.Diagnostic) bool {
	for _, d := range diags {
		if d.Severity == tfprotov6.DiagnosticSeverityError && d.Summary == "Missing Resource Identity After Read" {
			return true
		}
	}
	return false
}

And in Observe(), alongside the existing isResourceNotFoundDiags check:

isResourceNotFoundDiags := n.hasResourceNotFoundDiagnostic(readResponse.Diagnostics)
isMissingIdentityDiags := hasMissingResourceIdentityDiagnostic(readResponse.Diagnostics)
if fatalDiags := getFatalDiagnostics(readResponse.Diagnostics); fatalDiags != nil {
    if !isResourceNotFoundDiags && !isMissingIdentityDiags {
        n.opTracker.ResetReconstructedFrameworkTFState()
        return managed.ExternalObservation{}, errors.Wrap(fatalDiags, "read resource request failed")
    }
    if isResourceNotFoundDiags {
        n.logger.Debug("TF ReadResource returned error diagnostics, but XP resource was configured to treat them as `Resource not exists`. Skipping", "skippedDiags", fatalDiags)
    }
    if isMissingIdentityDiags {
        n.logger.Debug("TF ReadResource returned Missing Resource Identity diagnostic, treating as resource not found", "skippedDiags", fatalDiags)
        isResourceNotFoundDiags = true
    }
}

Without this, any Plugin Framework resource with an IdentitySchema will fail on first reconciliation (resource doesn't exist yet) or when the external resource is deleted out-of-band.

aditmeno added a commit to canary-technologies-corp/upjet that referenced this pull request Apr 3, 2026
When a resource with an IdentitySchema is deleted externally, the
terraform-plugin-framework's ReadResource returns a null state. The
AWS provider's identity interceptor skips populating identity for null
states, but the framework still validates that identity must be non-null
after a successful Read - producing the "Missing Resource Identity After
Read" error.

This change treats that specific diagnostic as a resource-not-found
condition, allowing Upjet to correctly detect that the external resource
no longer exists and proceed with recreation.

Complements the identity propagation from PR crossplane#623.

Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>
When a TF Plugin Framework resource with an IdentitySchema is deleted
externally or doesn't yet exist, the provider's identity interceptor
skips populating identity for null states, causing terraform-plugin-framework
to return a "Missing Resource Identity After Read" error diagnostic.

Treat this diagnostic as a resource-not-found condition so that Observe
correctly reports ResourceExists: false instead of failing.

Signed-off-by: Erik Miller <erik.miller@gusto.com>
@erikmiller-gusto erikmiller-gusto force-pushed the feat/resource-identity-support branch from 416e927 to b1d9689 Compare April 3, 2026 18:49
@erikmiller-gusto
Copy link
Copy Markdown
Author

@aditmeno I've updated the PR to address that. Will also run that through my own tests to confirm

@erikmiller-gusto
Copy link
Copy Markdown
Author

@aditmeno was able to confirm the external deletion flow on the aws Bucketlifecycleconfiguration

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.

[Bug]: Unable to create securitygroupegressrule or securitygroupingressrule in v2.5.0

2 participants