Skip to content

fix(tfpluginsdk): fix ignore changes path for converted singleton lists#633

Open
castorw wants to merge 2 commits intocrossplane:mainfrom
castorw:fix-singleton-ignore-changes
Open

fix(tfpluginsdk): fix ignore changes path for converted singleton lists#633
castorw wants to merge 2 commits intocrossplane:mainfrom
castorw:fix-singleton-ignore-changes

Conversation

@castorw
Copy link
Copy Markdown

@castorw castorw commented Apr 20, 2026

Description of your changes

Fixes crossplane-contrib/provider-upjet-aws#2044
Fixes crossplane-contrib/provider-upjet-aws#1907

This PR updates the process of ignore_changes paths computation which did not handle converted singleton lists properly. Resources represented as objects in CRDs and blocks/lists were added improperly to the ignore_changes list. Example (and apparently the most usual use case) for eks.aws.m.upbound.io/v1beta1/NodeGroup:

  • scaling_config.0.desired_size is the expected output and correct value,
  • scaling_config.desired_size is the invalid output produced by the plugin.

Here we have added the missing .0 path segment to circumvent this issue.
Fixing this should resolve the underlying issue for all resource types but primarily fixing issue preventing use of automated scaling (eg. cluster-autoscaler) for EKS clusters.

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

Steps to reproduce the issue are detailed in my original issue crossplane-contrib/provider-upjet-aws#2044. Here's a guideline for testing:

  1. Create eks.aws.m.upbound.io/v1beta1/Cluster,
  2. Create eks.aws.m.upbound.io/v1beta1/NodeGroup with the following options:
apiVersion: eks.aws.m.upbound.io/v1beta1
kind: NodeGroup
metadata:
  [...]
spec:
  managementPolicies:
    - Create
    - Update
    - Delete
    - Observe
  initProvider:
    scalingConfig:
      desiredSize: 5
  forProvider:
    scalingConfig:
      maxSize: 10
      minSize: 3
  [...]
  1. Using AWS console or CLI update the desired cluster size to different value,
  2. Provider should not touch the provisioned node group resource - in the original codebase it would as invalid path was present in ignore_changes (scaling_config.desired_size vs scaling_config.0.desired_size).

Signed-off-by: Lubomir Kaplan <castor@castor.sk>
@castorw castorw force-pushed the fix-singleton-ignore-changes branch from 256ed6a to 2781a2c Compare April 20, 2026 11:07
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 20, 2026

📝 Walkthrough

Walkthrough

The change extends filterInitExclusiveDiffs to accept a resource configuration parameter and converts Terraform ignore-change keys for singleton list expansions. It rewrites ignored parameter keys by inserting index segments (e.g., converting path.field to path.0.field) when they match TFListConversionPaths() prefixes.

Changes

Cohort / File(s) Summary
Diff Filtering Logic for Singleton List Expansions
pkg/controller/external_tfpluginsdk.go
Enhanced filterInitExclusiveDiffs to accept config *config.Resource parameter and added logic to rewrite ignored keys by inserting 0 index segments for paths matching singleton list conversion prefixes. Existing behavior for removing attributes and collection length keys remains unchanged.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

backport release-2.2

Suggested reviewers

  • ulucinar
  • sergenyalcin
  • erhancagirici
🚥 Pre-merge checks | ✅ 7
✅ Passed checks (7 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the issue, provides reproducible steps, and describes how the fix addresses the problem with singleton list path expansion.
Linked Issues check ✅ Passed The code changes directly implement the fix for both linked issues (#2044, #1907) by adding logic to rewrite ignore-change keys with the missing .0 index for singleton list conversions.
Out of Scope Changes check ✅ Passed All changes in the PR are directly related to fixing the singleton list path expansion issue; no out-of-scope modifications are present.
Configuration Api Breaking Changes ✅ Passed The PR only modifies pkg/controller/external_tfpluginsdk.go, an unexported function. Shell results confirm no pkg/config files were modified, so no breaking change label is required.
Generated Code Manual Edits ✅ Passed File pkg/controller/external_tfpluginsdk.go is manually-maintained controller logic, not generated code, so manual edits are appropriate.
Template Breaking Changes ✅ Passed The modified file is core runtime source code, not a template file; actual templates are .tmpl files in pkg/pipeline/templates/. The private function change is a targeted runtime fix for singleton list path conversions, not code generation affecting all providers.
Title check ✅ Passed The title clearly and concisely describes the main change: fixing ignore changes paths for converted singleton lists in the tfpluginsdk, staying within the 72-character requirement.

✏️ 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: 1

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_tfpluginsdk.go (1)

391-403: ⚠️ Potential issue | 🟠 Major

Clear the singleton-list container length key too.

After Line 349 converts scaling_config.desired_size to scaling_config.0.desired_size, this loop computes scaling_config.0.#; the Plugin SDK list length diff is scaling_config.#. If that parent length key remains, the diff can still look non-empty and trigger the update loop this PR is trying to prevent.

🐛 Suggested addition for converted singleton prefixes
 		for _, lengthSymbol := range []string{"%", "#"} {
 			keyComponents[len(keyComponents)-1] = lengthSymbol
 			lengthKey := strings.Join(keyComponents, ".")
 			delete(instanceDiff.Attributes, lengthKey)
 		}
+
+		for _, p := range config.TFListConversionPaths() {
+			p = normalizeTFListConversionPath(p)
+			convertedPrefix := fmt.Sprintf("%s.0.", p)
+			if !strings.HasPrefix(keyToIgnore, convertedPrefix) {
+				continue
+			}
+			delete(instanceDiff.Attributes, fmt.Sprintf("%s.#", p))
+			delete(instanceDiff.Attributes, fmt.Sprintf("%s.%%", p))
+		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/controller/external_tfpluginsdk.go` around lines 391 - 403, The loop over
initProviderExclusiveParamKeys currently deletes length keys like
"scaling_config.0.#" but misses the parent list length "scaling_config.#", so
keep causing spurious diffs; update the loop that builds lengthKey (in the same
block operating on instanceDiff.Attributes) to also compute and delete the
corresponding parent-length key when a singleton index was inserted —
specifically, after creating lengthKey from keyComponents, if the component
immediately before the length symbol is "0", construct parentLengthKey by
removing that ".0" segment (e.g., turn "scaling_config.0.#" into
"scaling_config.#") and call delete(instanceDiff.Attributes, parentLengthKey) as
well.
🤖 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_tfpluginsdk.go`:
- Around line 342-358: Normalize TFListConversionPaths() entries by replacing
"[0]" and "[*]" with "0" and trimming/ensuring dot separators, sort the
normalized paths by descending length so longer prefixes match first, then
iterate over those normalizedPaths in the loop that builds
initProviderExclusiveParamKeysConverted (use matchPrefix := fmt.Sprintf("%s.",
normalizedPath)) and on first match replace the prefix with fmt.Sprintf("%s0.",
matchPrefix) and break the inner loop to avoid duplicate partial conversions;
add table-driven unit tests exercising patterns with "[0]", "[*]", nested and
overlapping paths to ensure one deterministic converted key per input.

---

Outside diff comments:
In `@pkg/controller/external_tfpluginsdk.go`:
- Around line 391-403: The loop over initProviderExclusiveParamKeys currently
deletes length keys like "scaling_config.0.#" but misses the parent list length
"scaling_config.#", so keep causing spurious diffs; update the loop that builds
lengthKey (in the same block operating on instanceDiff.Attributes) to also
compute and delete the corresponding parent-length key when a singleton index
was inserted — specifically, after creating lengthKey from keyComponents, if the
component immediately before the length symbol is "0", construct parentLengthKey
by removing that ".0" segment (e.g., turn "scaling_config.0.#" into
"scaling_config.#") and call delete(instanceDiff.Attributes, parentLengthKey) as
well.
🪄 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: ce6e0bbf-f5c7-4f66-88df-c0cfaba916c6

📥 Commits

Reviewing files that changed from the base of the PR and between c6d5213 and 256ed6a.

📒 Files selected for processing (1)
  • pkg/controller/external_tfpluginsdk.go

Comment on lines +342 to +358
/* process singleton list expansions */
initProviderExclusiveParamKeysConverted := []string{}
for _, key := range initProviderExclusiveParamKeys {
matched := false
for _, p := range config.TFListConversionPaths() {
matchPrefix := fmt.Sprintf("%s.", p)
if strings.HasPrefix(key, matchPrefix) {
initProviderExclusiveParamKeysConverted = append(initProviderExclusiveParamKeysConverted, strings.Replace(key, matchPrefix, fmt.Sprintf("%s0.", matchPrefix), 1))
matched = true
continue
}
}
if !matched {
initProviderExclusiveParamKeysConverted = append(initProviderExclusiveParamKeysConverted, key)
}
}
initProviderExclusiveParamKeys = initProviderExclusiveParamKeysConverted
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Normalize and apply singleton-list conversions deterministically.

Thanks for fixing the simple scaling_config case. This loop still compares raw TFListConversionPaths() values directly, but those paths may contain [0]/[*]; overlapping singleton paths can also append duplicate partial keys because continue stays inside the inner loop. Please normalize paths and produce one fully converted key per input, ideally applying longer prefixes first.

🐛 Suggested direction
 import (
 	"context"
 	"fmt"
+	"sort"
 	"strings"
 	"time"
@@
+func normalizeTFListConversionPath(p string) string {
+	p = strings.ReplaceAll(p, "[*]", "")
+	return strings.ReplaceAll(p, "[0]", "")
+}
+
 func filterInitExclusiveDiffs(config *config.Resource, tr resource.Terraformed, instanceDiff *tf.InstanceDiff) error { //nolint:gocyclo
@@
 	/* process singleton list expansions */
-	initProviderExclusiveParamKeysConverted := []string{}
+	conversionPaths := config.TFListConversionPaths()
+	sort.Slice(conversionPaths, func(i, j int) bool {
+		return len(normalizeTFListConversionPath(conversionPaths[i])) > len(normalizeTFListConversionPath(conversionPaths[j]))
+	})
+
+	initProviderExclusiveParamKeysConverted := make([]string, 0, len(initProviderExclusiveParamKeys))
 	for _, key := range initProviderExclusiveParamKeys {
-		matched := false
-		for _, p := range config.TFListConversionPaths() {
+		convertedKey := key
+		for _, p := range conversionPaths {
+			p = normalizeTFListConversionPath(p)
 			matchPrefix := fmt.Sprintf("%s.", p)
-			if strings.HasPrefix(key, matchPrefix) {
-				initProviderExclusiveParamKeysConverted = append(initProviderExclusiveParamKeysConverted, strings.Replace(key, matchPrefix, fmt.Sprintf("%s0.", matchPrefix), 1))
-				matched = true
-				continue
+			if strings.HasPrefix(convertedKey, matchPrefix) {
+				convertedKey = strings.Replace(convertedKey, matchPrefix, fmt.Sprintf("%s0.", matchPrefix), 1)
 			}
 		}
-		if !matched {
-			initProviderExclusiveParamKeysConverted = append(initProviderExclusiveParamKeysConverted, key)
-		}
+		initProviderExclusiveParamKeysConverted = append(initProviderExclusiveParamKeysConverted, convertedKey)
 	}

Please add table-driven coverage for [0], [*], and nested/overlapping conversion paths. As per coding guidelines, All Upjet code must be covered by tests. Do not use Ginkgo tests or third party testing libraries; use Go's standard testing approach. 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/controller/external_tfpluginsdk.go` around lines 342 - 358, Normalize
TFListConversionPaths() entries by replacing "[0]" and "[*]" with "0" and
trimming/ensuring dot separators, sort the normalized paths by descending length
so longer prefixes match first, then iterate over those normalizedPaths in the
loop that builds initProviderExclusiveParamKeysConverted (use matchPrefix :=
fmt.Sprintf("%s.", normalizedPath)) and on first match replace the prefix with
fmt.Sprintf("%s0.", matchPrefix) and break the inner loop to avoid duplicate
partial conversions; add table-driven unit tests exercising patterns with "[0]",
"[*]", nested and overlapping paths to ensure one deterministic converted key
per input.

Signed-off-by: Lubomir Kaplan <castor@castor.sk>
@castorw castorw changed the title fix(tfpluginsdk): expand ignore changes path for converted singleton lists fix(tfpluginsdk): fix ignore changes path for converted singleton lists Apr 20, 2026
@Upbound-CLA
Copy link
Copy Markdown

Upbound-CLA commented Apr 30, 2026

CLA assistant check
All committers have signed the CLA.

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

Projects

None yet

2 participants