Skip to content

Commit 10ceb35

Browse files
EspenAlbertlantoli
andauthored
chore: Adds support for conditionally keepUnknown based on state value (node_count and auto_scaling fix) (#3127)
* refactor: Enhance CopyUnknowns functionality with call for keepUnknown * refactor: Support calls in CopyUnknowns function * refactor: Use conditional calls for node_count and auto_scaling attributes * refactor: Introduce AutoScaling model and enhance TestCopyUnknowns * chore: minor comment refactor * refactor: Clarify comments and apply PR suggestions * refactor: Add additional test steps for advanced_cluster to demonstrate No Plan Changes when removing a block & Unexpected Plan Changes when removing `advanced_configuration` block * refactor: Comment out Mocked Acceptance Tests in workflow to demonstrate test failures * fix: non-empty refresh plan for advanced_configuration * cannot use a Default, that will trigger infinite loop plan change * Revert "refactor: Comment out Mocked Acceptance Tests in workflow to demonstrate test failures" This reverts commit 3ae511a. * use type KeepUnknownFunc * no need to use calls at root level * revert change to change_stream_options_pre_and_post_images_expire_after_seconds * Reapply "refactor: Comment out Mocked Acceptance Tests in workflow to demonstrate test failures" This reverts commit f1faf85. * Revert "fix: non-empty refresh plan for advanced_configuration" This reverts commit a30c3ae. * comment checks * Revert "Reapply "refactor: Comment out Mocked Acceptance Tests in workflow to demonstrate test failures"" This reverts commit 2942443. * comment test step --------- Co-authored-by: Leo Antoli <[email protected]>
1 parent b03ab39 commit 10ceb35

File tree

4 files changed

+232
-26
lines changed

4 files changed

+232
-26
lines changed

internal/common/schemafunc/plan_modifier_state_for_unknown.go

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,38 +35,58 @@ func HasUnknowns(obj any) bool {
3535
return false
3636
}
3737

38+
type KeepUnknownFunc func(string, attr.Value) bool
39+
3840
// CopyUnknowns use reflection to copy unknown fields from src to dest.
3941
// The implementation is similar to internal/common/conversion/model_generation.go#CopyModel
4042
// keepUnknown is a list of fields that should not be copied, should always use the TF config name (snake_case)
4143
// nestedStructMapping is a map of field names to their type: object, list. (`set` not implemented yet)
42-
func CopyUnknowns(ctx context.Context, src, dest any, keepUnknown []string) {
44+
func CopyUnknowns(ctx context.Context, src, dest any, keepUnknown []string, keepUnknownCall KeepUnknownFunc) {
4345
validateKeepUnknown(keepUnknown)
46+
slicesContains := func(name string, value attr.Value) bool {
47+
return slices.Contains(keepUnknown, name)
48+
}
49+
copyUnknowns(ctx, src, dest, KeepUnknownFuncOr(slicesContains, keepUnknownCall))
50+
}
51+
52+
func KeepUnknownFuncOr(calls ...KeepUnknownFunc) KeepUnknownFunc {
53+
return func(name string, value attr.Value) bool {
54+
for _, call := range calls {
55+
if call != nil && call(name, value) {
56+
return true
57+
}
58+
}
59+
return false
60+
}
61+
}
62+
63+
func copyUnknowns(ctx context.Context, src, dest any, keepUnknownCall KeepUnknownFunc) {
4464
valSrc, valDest := validateStructPointers(src, dest)
4565
typeSrc := valSrc.Type()
4666
typeDest := valDest.Type()
4767
for i := range typeDest.NumField() {
4868
fieldDest := typeDest.Field(i)
4969
name, tfName := fieldNameTFName(&fieldDest)
50-
if slices.Contains(keepUnknown, tfName) {
70+
srcValue := valSrc.FieldByName(name).Interface()
71+
if keepUnknownCall(tfName, srcValue.(attr.Value)) {
5172
continue
5273
}
5374
_, found := typeSrc.FieldByName(name)
5475
if !found || !valDest.Field(i).CanSet() {
5576
continue
5677
}
57-
nestedSrc := valSrc.FieldByName(name).Interface()
5878
nestedDest := valDest.FieldByName(name).Interface()
59-
objValueSrc, okSrc := nestedSrc.(types.Object)
79+
objValueSrc, okSrc := srcValue.(types.Object)
6080
objValueDest, okDest := nestedDest.(types.Object)
6181
if okSrc && okDest {
62-
objValueNew := copyUnknownsFromObject(ctx, objValueSrc, objValueDest, keepUnknown)
82+
objValueNew := copyUnknownsFromObject(ctx, objValueSrc, objValueDest, keepUnknownCall)
6383
valDest.Field(i).Set(reflect.ValueOf(objValueNew))
6484
continue
6585
}
66-
listValueSrc, okSrc := nestedSrc.(types.List)
86+
listValueSrc, okSrc := srcValue.(types.List)
6787
listValueDest, okDest := nestedDest.(types.List)
6888
if okSrc && okDest {
69-
listValueNew := copyUnknownsFromList(ctx, listValueSrc, listValueDest, keepUnknown)
89+
listValueNew := copyUnknownsFromList(ctx, listValueSrc, listValueDest, keepUnknownCall)
7090
valDest.Field(i).Set(reflect.ValueOf(listValueNew))
7191
continue
7292
}
@@ -130,7 +150,7 @@ func validateKeepUnknown(keepUnknown []string) {
130150
}
131151
}
132152

133-
func copyUnknownsFromObject(ctx context.Context, src, dest types.Object, keepUnknown []string) types.Object {
153+
func copyUnknownsFromObject(ctx context.Context, src, dest types.Object, keepUnknownCall func(string, attr.Value) bool) types.Object {
134154
// if something is null in the state and unknown in plan, we expect it to remain null
135155
if src.IsNull() && dest.IsUnknown() {
136156
return src
@@ -147,7 +167,8 @@ func copyUnknownsFromObject(ctx context.Context, src, dest types.Object, keepUnk
147167
attributesDest = fillUnknowns(ctx, attributesSrc)
148168
}
149169
for name, attr := range attributesDest {
150-
if slices.Contains(keepUnknown, name) {
170+
replacement := attributesSrc[name]
171+
if keepUnknownCall(name, replacement) {
151172
attributesMerged[name] = attr
152173
continue
153174
}
@@ -157,9 +178,9 @@ func copyUnknownsFromObject(ctx context.Context, src, dest types.Object, keepUnk
157178
tflog.Info(ctx, fmt.Sprintf("Copying unknown field: %s\n", name))
158179
switch {
159180
case isObject:
160-
attr = copyUnknownsFromObject(ctx, attributesSrc[name].(types.Object), tfObjectDest, keepUnknown)
181+
attr = copyUnknownsFromObject(ctx, attributesSrc[name].(types.Object), tfObjectDest, keepUnknownCall)
161182
case isList:
162-
attr = copyUnknownsFromList(ctx, attributesSrc[name].(types.List), tfListDest, keepUnknown)
183+
attr = copyUnknownsFromList(ctx, attributesSrc[name].(types.List), tfListDest, keepUnknownCall)
163184
default:
164185
attr = attributesSrc[name]
165186
}
@@ -168,11 +189,11 @@ func copyUnknownsFromObject(ctx context.Context, src, dest types.Object, keepUnk
168189
}
169190
if isList {
170191
tfListSrc := attributesSrc[name].(types.List)
171-
attr = copyUnknownsFromList(ctx, tfListSrc, tfListDest, keepUnknown)
192+
attr = copyUnknownsFromList(ctx, tfListSrc, tfListDest, keepUnknownCall)
172193
}
173194
if isObject {
174195
tfObjectSrc := attributesSrc[name].(types.Object)
175-
newObject := copyUnknownsFromObject(ctx, tfObjectSrc, tfObjectDest, keepUnknown)
196+
newObject := copyUnknownsFromObject(ctx, tfObjectSrc, tfObjectDest, keepUnknownCall)
176197
attr = newObject
177198
}
178199
attributesMerged[name] = attr
@@ -193,7 +214,7 @@ func fillUnknowns(ctx context.Context, attributesSrc map[string]attr.Value) map[
193214
return unknownAttributes
194215
}
195216

196-
func copyUnknownsFromList(ctx context.Context, src, dest types.List, keepUnknown []string) types.List {
217+
func copyUnknownsFromList(ctx context.Context, src, dest types.List, keepUnknownCall func(string, attr.Value) bool) types.List {
197218
srcElements := src.Elements()
198219
destElements := dest.Elements()
199220
count := len(srcElements)
@@ -204,7 +225,7 @@ func copyUnknownsFromList(ctx context.Context, src, dest types.List, keepUnknown
204225
for i := range count {
205226
srcObj := srcElements[i].(types.Object)
206227
destObj := destElements[i].(types.Object)
207-
newObj := copyUnknownsFromObject(ctx, srcObj, destObj, keepUnknown)
228+
newObj := copyUnknownsFromObject(ctx, srcObj, destObj, keepUnknownCall)
208229
merged[i] = newObj
209230
}
210231
return types.ListValueMust(dest.ElementType(ctx), merged)

internal/common/schemafunc/plan_modifier_state_for_unknown_test.go

Lines changed: 173 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package schemafunc_test
22

33
import (
44
"context"
5+
"fmt"
6+
"slices"
57
"testing"
68

79
"github.com/hashicorp/terraform-plugin-framework/attr"
@@ -85,6 +87,20 @@ func TestHasUnknown(t *testing.T) {
8587
}
8688
}
8789

90+
type TFAutoScalingModel struct {
91+
ComputeMinInstanceSize types.String `tfsdk:"compute_min_instance_size"`
92+
ComputeMaxInstanceSize types.String `tfsdk:"compute_max_instance_size"`
93+
ComputeEnabled types.Bool `tfsdk:"compute_enabled"`
94+
DiskGBEnabled types.Bool `tfsdk:"disk_gb_enabled"`
95+
}
96+
97+
var AutoScalingObjType = types.ObjectType{AttrTypes: map[string]attr.Type{
98+
"compute_enabled": types.BoolType,
99+
"disk_gb_enabled": types.BoolType,
100+
"compute_min_instance_size": types.StringType,
101+
"compute_max_instance_size": types.StringType,
102+
}}
103+
88104
type TFSpec struct {
89105
InstanceSize types.String `tfsdk:"instance_size"`
90106
NodeCount types.Int64 `tfsdk:"node_count"`
@@ -96,12 +112,14 @@ var SpecObjType = types.ObjectType{AttrTypes: map[string]attr.Type{
96112
}}
97113

98114
type TFRegionConfig struct {
115+
AutoScaling types.Object `tfsdk:"auto_scaling"`
99116
ProviderName types.String `tfsdk:"provider_name"`
100117
RegionName types.String `tfsdk:"region_name"`
101118
Spec types.Object `tfsdk:"spec"`
102119
}
103120

104121
var RegionConfigsObjType = types.ObjectType{AttrTypes: map[string]attr.Type{
122+
"auto_scaling": AutoScalingObjType,
105123
"provider_name": types.StringType,
106124
"region_name": types.StringType,
107125
"spec": SpecObjType,
@@ -136,35 +154,85 @@ type TFSimpleModel struct {
136154
var (
137155
ctx = context.Background()
138156
regionConfigSrc = TFRegionConfig{
157+
AutoScaling: autoScalingFalseAndNull,
139158
ProviderName: types.StringValue("aws"),
140159
RegionName: types.StringValue("US_EAST_1"),
141160
Spec: asObjectValue(ctx, TFSpec{InstanceSize: types.StringValue("M10"), NodeCount: types.Int64Value(3)}, SpecObjType.AttrTypes),
142161
}
162+
regionConfigNodeCount0 = TFRegionConfig{
163+
AutoScaling: autoScalingFalseAndNull,
164+
ProviderName: types.StringValue("aws"),
165+
RegionName: types.StringValue("US_EAST_1"),
166+
Spec: asObjectValue(ctx, TFSpec{InstanceSize: types.StringValue("M10"), NodeCount: types.Int64Value(0)}, SpecObjType.AttrTypes),
167+
}
143168
regionConfigDest = TFRegionConfig{
169+
AutoScaling: autoScalingFalseAndNull,
144170
ProviderName: types.StringUnknown(),
145171
RegionName: types.StringValue("US_EAST_1"),
146172
Spec: types.ObjectUnknown(SpecObjType.AttrTypes),
147173
}
148174
regionConfigNodeCountUnknown = TFRegionConfig{
175+
AutoScaling: autoScalingFalseAndNull,
149176
ProviderName: types.StringValue("aws"),
150177
RegionName: types.StringValue("US_EAST_1"),
151178
Spec: asObjectValue(ctx, TFSpec{InstanceSize: types.StringValue("M10"), NodeCount: types.Int64Unknown()}, SpecObjType.AttrTypes),
152179
}
153180
regionConfigSpecUnknown = TFRegionConfig{
181+
AutoScaling: autoScalingFalseAndNull,
154182
ProviderName: types.StringValue("aws"),
155183
RegionName: types.StringValue("US_EAST_1"),
156184
Spec: types.ObjectUnknown(SpecObjType.AttrTypes),
157185
}
158-
advancedConfigTrue = asObjectValue(ctx, TFAdvancedConfig{JavascriptEnabled: types.BoolValue(true)}, AdvancedConfigObjType.AttrTypes)
186+
advancedConfigTrue = asObjectValue(ctx, TFAdvancedConfig{JavascriptEnabled: types.BoolValue(true)}, AdvancedConfigObjType.AttrTypes)
187+
autoScalingFalseAndEmpty = asObjectValue(ctx, TFAutoScalingModel{
188+
ComputeEnabled: types.BoolValue(false),
189+
DiskGBEnabled: types.BoolValue(false),
190+
ComputeMinInstanceSize: types.StringValue(""),
191+
ComputeMaxInstanceSize: types.StringValue(""),
192+
}, AutoScalingObjType.AttrTypes)
193+
194+
autoScalingFalseAndNull = asObjectValue(ctx, TFAutoScalingModel{
195+
ComputeEnabled: types.BoolValue(false),
196+
DiskGBEnabled: types.BoolValue(false),
197+
ComputeMinInstanceSize: types.StringNull(),
198+
ComputeMaxInstanceSize: types.StringNull(),
199+
}, AutoScalingObjType.AttrTypes)
200+
autoScalingTrueAndNonEmpty = asObjectValue(ctx, TFAutoScalingModel{
201+
ComputeEnabled: types.BoolValue(true),
202+
DiskGBEnabled: types.BoolValue(true),
203+
ComputeMinInstanceSize: types.StringValue("M10"),
204+
ComputeMaxInstanceSize: types.StringValue("M20"),
205+
}, AutoScalingObjType.AttrTypes)
206+
autoScalingUnknown = types.ObjectUnknown(AutoScalingObjType.AttrTypes)
207+
autoScalingLeafsUnknown = asObjectValue(ctx, TFAutoScalingModel{
208+
ComputeEnabled: types.BoolUnknown(),
209+
DiskGBEnabled: types.BoolUnknown(),
210+
ComputeMinInstanceSize: types.StringUnknown(),
211+
ComputeMaxInstanceSize: types.StringUnknown(),
212+
}, AutoScalingObjType.AttrTypes)
213+
keepProjectIDUnknown = func(name string, value attr.Value) bool {
214+
return name == "project_id"
215+
}
216+
useStateOnlyWhenNodeCount0 = func(name string, value attr.Value) bool {
217+
return name == "node_count" && !value.Equal(types.Int64Value(0))
218+
}
219+
220+
autoScalingBoolsKeepUnknown = func(name string, value attr.Value) bool {
221+
return slices.Contains([]string{"compute_enabled", "disk_gb_enabled"}, name) && value.Equal(types.BoolValue(true))
222+
}
223+
autoScalingStringsKeepUnknown = func(name string, value attr.Value) bool {
224+
return slices.Contains([]string{"compute_min_instance_size", "compute_max_instance_size"}, name) && value.(types.String).ValueString() != ""
225+
}
159226
)
160227

161228
func TestCopyUnknowns(t *testing.T) {
162229
tests := map[string]struct {
163-
src *TFSimpleModel
164-
dest *TFSimpleModel
165-
expectedDest *TFSimpleModel
166-
panicMessage string
167-
keepUnknown []string
230+
src *TFSimpleModel
231+
dest *TFSimpleModel
232+
expectedDest *TFSimpleModel
233+
keepUnknownCalls schemafunc.KeepUnknownFunc
234+
panicMessage string
235+
keepUnknown []string
168236
}{
169237
"copy unknown basic fields": {
170238
src: &TFSimpleModel{
@@ -225,6 +293,93 @@ func TestCopyUnknowns(t *testing.T) {
225293
},
226294
keepUnknown: []string{"spec"},
227295
},
296+
"respect keepUnknownCall root": {
297+
src: &TFSimpleModel{
298+
ProjectID: types.StringValue("src-project"),
299+
Name: types.StringValue("src-name"),
300+
},
301+
dest: &TFSimpleModel{
302+
ProjectID: types.StringUnknown(),
303+
Name: types.StringUnknown(),
304+
},
305+
expectedDest: &TFSimpleModel{
306+
ProjectID: types.StringUnknown(),
307+
Name: types.StringValue("src-name"),
308+
},
309+
keepUnknownCalls: keepProjectIDUnknown,
310+
},
311+
"respect keepUnknownCall nested": {
312+
src: &TFSimpleModel{
313+
ProjectID: types.StringValue("src-project"),
314+
Name: types.StringValue("src-name"),
315+
ReplicationSpecs: newReplicationSpecs(ctx, types.StringValue("Zone 1"), []TFRegionConfig{regionConfigSrc}),
316+
},
317+
dest: &TFSimpleModel{
318+
ProjectID: types.StringUnknown(),
319+
Name: types.StringUnknown(),
320+
ReplicationSpecs: newReplicationSpecs(ctx, types.StringUnknown(), []TFRegionConfig{regionConfigNodeCountUnknown}),
321+
},
322+
expectedDest: &TFSimpleModel{
323+
ProjectID: types.StringValue("src-project"),
324+
Name: types.StringValue("src-name"),
325+
ReplicationSpecs: newReplicationSpecs(ctx, types.StringValue("Zone 1"), []TFRegionConfig{regionConfigNodeCountUnknown}),
326+
},
327+
keepUnknownCalls: useStateOnlyWhenNodeCount0,
328+
},
329+
"respect multiple keepUnknownCall": {
330+
src: &TFSimpleModel{
331+
ProjectID: types.StringValue("src-project"),
332+
Name: types.StringValue("src-name"),
333+
ReplicationSpecs: newReplicationSpecs(ctx, types.StringValue("Zone 1"), []TFRegionConfig{regionConfigSrc}),
334+
},
335+
dest: &TFSimpleModel{
336+
ProjectID: types.StringUnknown(),
337+
Name: types.StringUnknown(),
338+
ReplicationSpecs: newReplicationSpecs(ctx, types.StringUnknown(), []TFRegionConfig{regionConfigNodeCountUnknown}),
339+
},
340+
expectedDest: &TFSimpleModel{
341+
ProjectID: types.StringUnknown(),
342+
Name: types.StringValue("src-name"),
343+
ReplicationSpecs: newReplicationSpecs(ctx, types.StringValue("Zone 1"), []TFRegionConfig{regionConfigNodeCountUnknown}),
344+
},
345+
keepUnknownCalls: schemafunc.KeepUnknownFuncOr(keepProjectIDUnknown, useStateOnlyWhenNodeCount0),
346+
},
347+
"copy node_count 0": {
348+
src: &TFSimpleModel{
349+
ReplicationSpecs: newReplicationSpecs(ctx, types.StringValue("Zone 1"), []TFRegionConfig{regionConfigNodeCount0}),
350+
},
351+
dest: &TFSimpleModel{
352+
ReplicationSpecs: newReplicationSpecs(ctx, types.StringUnknown(), []TFRegionConfig{regionConfigNodeCountUnknown}),
353+
},
354+
expectedDest: &TFSimpleModel{
355+
ReplicationSpecs: newReplicationSpecs(ctx, types.StringValue("Zone 1"), []TFRegionConfig{regionConfigNodeCount0}),
356+
},
357+
keepUnknownCalls: useStateOnlyWhenNodeCount0,
358+
},
359+
"keepUnknownCall on string": {
360+
src: &TFSimpleModel{
361+
ReplicationSpecs: newReplicationSpecs(ctx, types.StringValue("Zone 1"), []TFRegionConfig{
362+
regionConfigWithAutoScaling(autoScalingFalseAndEmpty),
363+
regionConfigWithAutoScaling(autoScalingFalseAndNull),
364+
regionConfigWithAutoScaling(autoScalingTrueAndNonEmpty),
365+
}),
366+
},
367+
dest: &TFSimpleModel{
368+
ReplicationSpecs: newReplicationSpecs(ctx, types.StringUnknown(), []TFRegionConfig{
369+
regionConfigWithAutoScaling(autoScalingUnknown),
370+
regionConfigWithAutoScaling(autoScalingUnknown),
371+
regionConfigWithAutoScaling(autoScalingUnknown),
372+
}),
373+
},
374+
expectedDest: &TFSimpleModel{
375+
ReplicationSpecs: newReplicationSpecs(ctx, types.StringValue("Zone 1"), []TFRegionConfig{
376+
regionConfigWithAutoScaling(autoScalingFalseAndEmpty),
377+
regionConfigWithAutoScaling(autoScalingFalseAndNull),
378+
regionConfigWithAutoScaling(autoScalingLeafsUnknown),
379+
}),
380+
},
381+
keepUnknownCalls: schemafunc.KeepUnknownFuncOr(autoScalingStringsKeepUnknown, autoScalingBoolsKeepUnknown),
382+
},
228383
"non-pointer input": {
229384
src: &TFSimpleModel{},
230385
dest: nil,
@@ -301,11 +456,11 @@ func TestCopyUnknowns(t *testing.T) {
301456
t.Run(name, func(t *testing.T) {
302457
if tc.panicMessage != "" {
303458
assert.PanicsWithValue(t, tc.panicMessage, func() {
304-
schemafunc.CopyUnknowns(ctx, tc.src, tc.dest, tc.keepUnknown)
459+
schemafunc.CopyUnknowns(ctx, tc.src, tc.dest, tc.keepUnknown, nil)
305460
})
306461
return
307462
}
308-
schemafunc.CopyUnknowns(ctx, tc.src, tc.dest, tc.keepUnknown)
463+
schemafunc.CopyUnknowns(ctx, tc.src, tc.dest, tc.keepUnknown, tc.keepUnknownCalls)
309464
assert.Equal(t, *tc.expectedDest, *tc.dest)
310465
})
311466
}
@@ -324,7 +479,7 @@ func newReplicationSpecs(ctx context.Context, zoneName types.String, regionConfi
324479
for i, config := range regionConfigs {
325480
configObject, diags := types.ObjectValueFrom(ctx, RegionConfigsObjType.AttrTypes, config)
326481
if diags.HasError() {
327-
panic("failed to create region config object")
482+
panic(fmt.Sprintf("failed to create region config object %v", diags))
328483
}
329484
regionConfigsObjects[i] = configObject
330485
}
@@ -346,3 +501,12 @@ func combineReplicationSpecs(specs ...types.List) types.List {
346501
}
347502
return types.ListValueMust(ReplicationSpecsObjType, combined)
348503
}
504+
505+
func regionConfigWithAutoScaling(autoScaling types.Object) TFRegionConfig {
506+
return TFRegionConfig{
507+
AutoScaling: autoScaling,
508+
ProviderName: types.StringValue("aws"),
509+
RegionName: types.StringValue("US_EAST_1"),
510+
Spec: asObjectValue(ctx, TFSpec{InstanceSize: types.StringValue("M10"), NodeCount: types.Int64Value(3)}, SpecObjType.AttrTypes),
511+
}
512+
}

0 commit comments

Comments
 (0)