diff --git a/pkg/controller/external_tfpluginfw.go b/pkg/controller/external_tfpluginfw.go index f0307f8f..2a35a6fe 100644 --- a/pkg/controller/external_tfpluginfw.go +++ b/pkg/controller/external_tfpluginfw.go @@ -357,11 +357,21 @@ func (c *TerraformPluginFrameworkConnector) getResourceConfigTerraformValue(ctx // when only computed attributes or not-specified argument diffs // exist in the raw diff and no actual diff exists in the // parametrizable attributes. +// Exception: a diff where the prior value (Value2) is non-null and the +// planned value (Value1) is null represents an explicit removal of a +// previously-set optional attribute and must not be filtered. func (n *terraformPluginFrameworkExternalClient) filteredDiffExists(rawDiff []tftypes.ValueDiff) bool { filteredDiff := make([]tftypes.ValueDiff, 0) for _, diff := range rawDiff { + // Keep diffs where the planned value is non-null and known. if diff.Value1 != nil && diff.Value1.IsKnown() && !diff.Value1.IsNull() { filteredDiff = append(filteredDiff, diff) + continue + } + // Keep diffs where the prior value was non-null and the planned value + // is null — this is an explicit removal of a previously-set attribute. + if diff.Value1 != nil && diff.Value1.IsNull() && diff.Value2 != nil && !diff.Value2.IsNull() { + filteredDiff = append(filteredDiff, diff) } } return len(filteredDiff) > 0 diff --git a/pkg/controller/external_tfpluginfw_test.go b/pkg/controller/external_tfpluginfw_test.go index c20cd25f..9dff04c0 100644 --- a/pkg/controller/external_tfpluginfw_test.go +++ b/pkg/controller/external_tfpluginfw_test.go @@ -1272,3 +1272,91 @@ func newMockTPFResourceWithIdentity() *mockTPFResourceWithIdentity { }, } } + +func TestFilteredDiffExists(t *testing.T) { + strVal := func(s string) *tftypes.Value { + v := tftypes.NewValue(tftypes.String, s) + return &v + } + nullVal := func() *tftypes.Value { + v := tftypes.NewValue(tftypes.String, nil) + return &v + } + unknownVal := func() *tftypes.Value { + v := tftypes.NewValue(tftypes.String, tftypes.UnknownValue) + return &v + } + + cases := map[string]struct { + rawDiff []tftypes.ValueDiff + want bool + }{ + "EmptyDiff": { + rawDiff: []tftypes.ValueDiff{}, + want: false, + }, + "PlannedNonNullPriorNull": { + rawDiff: []tftypes.ValueDiff{ + {Value1: strVal("foo"), Value2: nullVal()}, + }, + want: true, + }, + "PlannedNonNullPriorNonNull": { + rawDiff: []tftypes.ValueDiff{ + {Value1: strVal("new"), Value2: strVal("old")}, + }, + want: true, + }, + // Explicit removal: prior was set, planned is null. The fix ensures + // this is not filtered out. + "PlannedNullPriorNonNull": { + rawDiff: []tftypes.ValueDiff{ + {Value1: nullVal(), Value2: strVal("foo")}, + }, + want: true, + }, + // Field was never specified; both sides are null — no real diff. + "PlannedNullPriorNull": { + rawDiff: []tftypes.ValueDiff{ + {Value1: nullVal(), Value2: nullVal()}, + }, + want: false, + }, + // Value1 nil means the child attribute has no individual planned value + // (e.g. when its parent object is null). Should remain filtered. + "PlannedNilPriorNonNull": { + rawDiff: []tftypes.ValueDiff{ + {Value1: nil, Value2: strVal("foo")}, + }, + want: false, + }, + // Unknown planned value corresponds to a computed field — filtered. + "PlannedUnknownPriorNonNull": { + rawDiff: []tftypes.ValueDiff{ + {Value1: unknownVal(), Value2: strVal("foo")}, + }, + want: false, + }, + // Simulates optional nested object removal: child attribute diffs have + // nil Value1, but the parent-level diff has null Value1 / non-null + // Value2 and must be detected. + "NestedObjectRemoval": { + rawDiff: []tftypes.ValueDiff{ + {Value1: nil, Value2: strVal("ClusterIP")}, // child attr, Value1 nil + {Value1: nil, Value2: strVal("Cluster")}, // child attr, Value1 nil + {Value1: nullVal(), Value2: strVal("3")}, // parent object null → removal + }, + want: true, + }, + } + + client := &terraformPluginFrameworkExternalClient{} + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := client.filteredDiffExists(tc.rawDiff) + if got != tc.want { + t.Errorf("filteredDiffExists() = %v, want %v", got, tc.want) + } + }) + } +}