-
Notifications
You must be signed in to change notification settings - Fork 123
fix: tfpluginsdk nil panics when LateInit off #600
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -433,7 +433,27 @@ func getTimeoutParameters(config *config.Resource) map[string]any { //nolint:goc | |
| } | ||
|
|
||
| func (n *terraformPluginSDKExternal) getResourceDataDiff(tr resource.Terraformed, ctx context.Context, s *tf.InstanceState, resourceExists bool) (*tf.InstanceDiff, error) { //nolint:gocyclo | ||
| resourceConfig := tf.NewResourceConfigRaw(n.params) | ||
| // before we calculate the diff and thus what we send over into the terraform provider | ||
| // code, we construct the desired resource fully specified, by taking the observed | ||
| // state from the upstream resource and then apply the desired spec.forProvider of the crossplane | ||
| // resource over it. This mirrors what terraform/opentofu do, because some terraform providers | ||
| // do not like nil values when processing the desired resource. This is especially important | ||
| // when managementPolicies have LateInitialize excluded, which means spec.forProvider can | ||
| // be very sparse. | ||
| params := n.params | ||
| if resourceExists && s != nil && len(n.params) > 0 { | ||
| stateValueMap, err := n.fromInstanceStateToJSONMapForDiff(s) | ||
| if err == nil && len(stateValueMap) > 0 { | ||
| params = make(map[string]any, len(stateValueMap)+len(n.params)) | ||
| for k, v := range stateValueMap { | ||
| params[k] = v | ||
| } | ||
| for k, v := range n.params { | ||
| params[k] = v | ||
| } | ||
| } | ||
| } | ||
| resourceConfig := tf.NewResourceConfigRaw(params) | ||
|
Comment on lines
435
to
+456
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Find external_tfpluginfw.go and check its structure
fd -e go -g '*tfpluginfw*'Repository: crossplane/upjet Length of output: 184 🏁 Script executed: #!/bin/bash
# Search for getResourceDataDiff method in Framework implementation
rg "getResourceDataDiff" --type goRepository: crossplane/upjet Length of output: 432 🏁 Script executed: #!/bin/bash
# Get the full content of external_tfpluginfw.go to understand the diff computation
fd -e go -g '*tfpluginfw*' -x cat {} -nRepository: crossplane/upjet Length of output: 50372 🏁 Script executed: #!/bin/bash
# Look for the Observe method in external_tfpluginfw.go to see how it computes diffs
rg -A 50 "func \(n \*terraformPluginFrameworkExternalClient\) Observe" pkg/controller/external_tfpluginfw.goRepository: crossplane/upjet Length of output: 2386 🏁 Script executed: #!/bin/bash
# Search for any state-related or diff-related logic in the Framework implementation
rg "getDiffPlanResponse|filteredDiffExists|proposed|stateValueMap" pkg/controller/external_tfpluginfw.goRepository: crossplane/upjet Length of output: 2401 🏁 Script executed: #!/bin/bash
# Look for the proposedState function to understand how it computes proposed state
rg -B 5 -A 20 "func proposedState" pkg/controller/external_tfpluginfw.goRepository: crossplane/upjet Length of output: 42 🏁 Script executed: #!/bin/bash
# Check how getDiffPlanResponse is called in Observe method and if state merging happens
rg -B 10 -A 30 "planResponse, hasDiff, err := n.getDiffPlanResponse" pkg/controller/external_tfpluginfw.goRepository: crossplane/upjet Length of output: 1677 🏁 Script executed: #!/antml:thinking
Also need to understand:
1. Is there state merging before getDiffPlanResponse is called?
2. Look at the full Observe method to see the complete flow
</thinking>
#!/bin/bash
# Get full Observe method to see complete flow
rg -A 150 "func \(n \*terraformPluginFrameworkExternalClient\) Observe\(ctx context.Context, mg xpresource.Managed\)" pkg/controller/external_tfpluginfw.go | head -200Repository: crossplane/upjet Length of output: 289 🏁 Script executed: #!/bin/bash
# Look for proposedState function definition
rg -B 2 -A 15 "^func proposedState" pkg/controller/Repository: crossplane/upjet Length of output: 1342 🏁 Script executed: #!/bin/bash
# Check if Framework implementation merges state like SDK does before calling getDiffPlanResponse
rg -B 20 "planResponse, hasDiff, err := n.getDiffPlanResponse" pkg/controller/external_tfpluginfw.go | grep -A 20 "stateValueMap\|params\|merge"Repository: crossplane/upjet Length of output: 583 🏁 Script executed: #!/bin/bash
# Let's see the full context around where getDiffPlanResponse is called
sed -n '400,500p' pkg/controller/external_tfpluginfw.goRepository: crossplane/upjet Length of output: 4016 🏁 Script executed: #!/bin/bash
# Check if Framework's Connect method merges state like SDK version does
rg -A 5 "resourceTerraformConfigValue.*=" pkg/controller/external_tfpluginfw.go | head -20Repository: crossplane/upjet Length of output: 42 🏁 Script executed: #!/bin/bash
# Compare: look at SDK version's Connect to see when params are set
rg -B 5 -A 10 "n.params = " pkg/controller/external_tfpluginsdk.go | head -30Repository: crossplane/upjet Length of output: 42 🏁 Script executed: #!/bin/bash
# Look at the full getDiffPlanResponse in SDK to see the state merging
rg -B 5 -A 80 "func \(n \*terraformPluginSDKExternal\) getResourceDataDiff" pkg/controller/external_tfpluginsdk.goRepository: crossplane/upjet Length of output: 3494 🏁 Script executed: #!/bin/bash
# Look at the full proposedNew implementation to understand the merging logic
rg -A 100 "^func proposedNew" pkg/controller/proposed_state.go | head -150Repository: crossplane/upjet Length of output: 5555 🏁 Script executed: #!/antml:thinking>
I need to see the full proposedNew implementation to understand if it truly merges state with config in a way that prevents the partial-value panic.
</thinking>Repository: crossplane/upjet Length of output: 204 🏁 Script executed: #!/an/bin/bash
# Check how n.resourceTerraformConfigValue is set in Framework's Connect method
rg -B 5 -A 15 "resourceTerraformConfigValue:" pkg/controller/external_tfpluginfw.goRepository: crossplane/upjet Length of output: 998 🏁 Script executed: #!/bin/bash
# Check if resourceTerraformConfigValue is ever updated after Connect
rg "resourceTerraformConfigValue\s*=" pkg/controller/external_tfpluginfw.goRepository: crossplane/upjet Length of output: 42 🏁 Script executed: #!/bin/bash
# Look at how params is constructed in Framework's Connect vs SDK's Connect
rg -A 5 "params.*:=" pkg/controller/external_tfpluginfw.go | head -40Repository: crossplane/upjet Length of output: 405 🏁 Script executed: #!/bin/bash
# See the full picture of what happens with params in Framework Connect
sed -n '160,260p' pkg/controller/external_tfpluginfw.goRepository: crossplane/upjet Length of output: 4770 🏁 Script executed: #!/bin/bash
# Check if resourceConfigTFValue/resourceTerraformConfigValue is static or gets updated
rg "SetReconstructedFrameworkTFState\|SetFrameworkTFState" pkg/controller/external_tfpluginfw.go | head -20Repository: crossplane/upjet Length of output: 42 🏁 Script executed: #!/bin/bash
# Check what happens in getDiffPlanResponse - does it merge state with config like SDK does?
sed -n '350,410p' pkg/controller/external_tfpluginfw.goRepository: crossplane/upjet Length of output: 2864 🏁 Script executed: #!/bin/bash
# Compare: In SDK's getResourceDataDiff, state is merged. Does Framework do similar merging?
# Look at proposedState call to see what gets passed
rg -B 3 -A 3 "proposedState\(n.resourceSchema" pkg/controller/external_tfpluginfw.goRepository: crossplane/upjet Length of output: 489 🏁 Script executed: #!/bin/bash
# Check Framework's Create method to see how it handles partial params
rg -A 80 "func \(n \*terraformPluginFrameworkExternalClient\) Create\(ctx context.Context" pkg/controller/external_tfpluginfw.go | head -100Repository: crossplane/upjet Length of output: 3828 🏁 Script executed: #!/antml:thinking>
I need to check if Create/Update methods in Framework use getDiffPlanResponse or if they have a different code path that might not do the state merging.
</thinking>Repository: crossplane/upjet Length of output: 204 🏁 Script executed: #!/bin/bash
# Check Framework's Update method
rg -A 60 "func \(n \*terraformPluginFrameworkExternalClient\) Update\(ctx context.Context" pkg/controller/external_tfpluginfw.go | head -80Repository: crossplane/upjet Length of output: 2802 🏁 Script executed: #!/bin/bash
# Look at proposedNewAttributes to understand how it merges prior and config
rg -A 50 "^func proposedNewAttributes" pkg/controller/proposed_state.goRepository: crossplane/upjet Length of output: 1997 🏁 Script executed: #!/bin/bash
# Verify: In SDK's Connect, are params initially from spec.forProvider (potentially sparse)?
# Look at how params are constructed in SDK Connect
rg -B 5 -A 20 "func \(c \*TerraformSDKConnector\) Connect\(ctx context.Context" pkg/controller/external_tfpluginsdk.go | head -50Repository: crossplane/upjet Length of output: 42 🏁 Script executed: #!/bin/bash
# Check if Framework's resourceTerraformConfigValue gets updated during Observe or stays static
rg "resourceTerraformConfigValue" pkg/controller/external_tfpluginfw.goRepository: crossplane/upjet Length of output: 771 🏁 Script executed: #!/bin/bash
# Check if Framework's proposedState provides same protection as SDK's state merge
# Look at the exact logic in proposedNewAttributes for non-computed attributes
sed -n '90,150p' pkg/controller/proposed_state.goRepository: crossplane/upjet Length of output: 2368 🏁 Script executed: #!/bin/bash
# Confirm: Does Framework ever update params/config after initial Connect?
# Search for any updates to params or resourceTerraformConfigValue after Connect
rg "params\s*=" pkg/controller/external_tfpluginfw.go | grep -v "resourceTerraformConfigValue:"Repository: crossplane/upjet Length of output: 42 🏁 Script executed: #!/bin/bash
# Look at SDK's comment about why state merging is needed
sed -n '430,465p' pkg/controller/external_tfpluginsdk.go | head -40Repository: crossplane/upjet Length of output: 1733 Framework implementation ( Unlike the SDK implementation which re-merges state into The } else {
// For non-computed attributes, we always take the config value,
// even if it is null.
newV = configV
}When Consider opening a follow-up PR to apply a similar fix: either re-merge state into params during 🤖 Prompt for AI Agents |
||
| instanceDiff, err := schema.InternalMap(n.config.TerraformResource.Schema).Diff(ctx, s, resourceConfig, n.config.TerraformResource.CustomizeDiff, n.ts.Meta, false) | ||
| if err != nil { | ||
| return nil, errors.Wrap(err, "failed to get *terraform.InstanceDiff") | ||
|
|
@@ -817,3 +837,16 @@ func (n *terraformPluginSDKExternal) fromInstanceStateToJSONMap(newState *tf.Ins | |
| } | ||
| return stateValueMap, attrsAsCtyValue, nil | ||
| } | ||
|
|
||
| func (n *terraformPluginSDKExternal) fromInstanceStateToJSONMapForDiff(newState *tf.InstanceState) (map[string]interface{}, error) { | ||
| impliedType := n.config.TerraformResource.CoreConfigSchema().ImpliedType() | ||
| attrsAsCtyValue, err := newState.AttrsAsObjectValue(impliedType) | ||
| if err != nil { | ||
| return nil, errors.Wrap(err, "could not convert attrs to cty value") | ||
| } | ||
| stateValueMap, err := schema.StateValueToJSONMap(attrsAsCtyValue, impliedType) | ||
| if err != nil { | ||
| return nil, errors.Wrap(err, "could not convert instance state value to JSON") | ||
| } | ||
| return stateValueMap, nil | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The conversion error is silently discarded, leaving a silent fallback to the pre-fix panic path.
When
fromInstanceStateToJSONMapForDiffreturns an error (e.g. a schema mismatch), theerrvariable is scoped to the innerifblock and never logged or returned — the code silently falls back toparams = n.params, which is precisely the partial-value path this PR is fixing. There is no signal to a provider developer (or Upjet maintainer) that the state merge was skipped.At minimum, emit a debug/warning log so the fallback is observable. Depending on your resilience preference, you could also return the error (which would surface it in
Observe). What was the intent — always degrade gracefully, or only on truly transient conversion failures?🛡️ Proposed fix: log the error before falling back
if resourceExists && s != nil && len(n.params) > 0 { stateValueMap, err := n.fromInstanceStateToJSONMapForDiff(s) - if err == nil && len(stateValueMap) > 0 { + if err != nil { + n.logger.Debug("Failed to convert instance state to JSON map for diff; falling back to spec-only params", + "error", err) + } else if len(stateValueMap) > 0 { params = make(map[string]any, len(stateValueMap)+len(n.params)) for k, v := range stateValueMap { params[k] = v } for k, v := range n.params { params[k] = v } } }📝 Committable suggestion
🤖 Prompt for AI Agents