Skip to content

Commit db43793

Browse files
authored
chore: Adds support for plan modifier on replication_specs changed (#3083)
* feat: Index copying of replication specs * feat: Index copying of region_configs * naive implementation, always CopyUnknowns but has errors * feat: Support detecting ISS to set empty `Id` * test: Add failing test case for an unknown object with fields that should be kept unknown * feat: Enhance CopyUnknowns to respect keepUnknown for object attributes that are unknown * feat: Update CopyUnknowns to respect keepUnknown for region configurations * feat: Enhance handling of unknowns in replication specs with dynamic keepUnknown logic for changed replication specs * chore: update mock data * refactor: move clusterUseISS to plan_modifier * fix: patch_payload forceUpdateAttr must include the attribute * feat: Enhance sharding configuration handling with improved unknowns management and error diagnostics * feat: Improve unknowns handling in replication specs with dynamic keepUnknowns logic * chore: safeguard conversion to PATCH by never using "" in Id or ZoneId * feat: Refactor unknowns handling in sharding configuration and remove unused clusterUseISS function * feat: Add zone name matching logic to enhance unknowns handling in replication specs * chore: update MacT tests * refactor: write doc and use the same concepts everywhere and determineKeepUnknowns * refactor: update Update method to use config instead of plan to avoid sending `UseStateForUnknown` values * feat: Implement FindChanges function to detect differences between source and destination TFModels * feat: Add AttributeChanges struct and methods for tracking attribute modifications * refactor: Rename config variable to configModel to avoid shadowing config import * refactor: Enhance determineKeepUnknowns logic to incorporate attribute changes and clean up unused functions * refactor: Use AttributeChanges instead of Patchpayload * refactor: Use vars to simplify the logic of which fields to keep unknown * test: Add unit tests for KeepUnknown method in AttributeChanges * refactor: Simplify leafChanges logic and add ListLenChanges method for better list change detection * refactor: External_id should not be set if the size of replication specs changes * refactor: Add ListIndexChanged and NestedListLenChanges methods for improved change detection * refactor: Enhance handling of unknowns in replication specs and simplify related logic by using AttributeChanges * chore: update mocked data * chore: revert stylistic changes * doc: Add some comments to describe changes * refactor: Simplify patch options for replication specs in Update method * chore: minor comment [ci skip] * refactor: move fields with no dependencies to the top * feat: add minimize level configuration for replication specs * fix minimize level logic * fix: change default minimize level to always for bug investigation * merge plan modifier changes cleanup * fix: tenant upgrade unknown for flex for mongo_db_major_version * refactor: optimize variable declarations and improve comments in schema functions * fix: add mongo_db_major_version to keepUnknownFlexUpgrade for tenant upgrade (goes from "" --> x.x) * refactor: remove unnecessary handling of flex upgrade in useStateForUnknowns function
1 parent 07dbc9c commit db43793

File tree

13 files changed

+1009
-106
lines changed

13 files changed

+1009
-106
lines changed
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package schemafunc
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"slices"
7+
"strings"
8+
9+
"github.com/hashicorp/terraform-plugin-framework/attr"
10+
"github.com/hashicorp/terraform-plugin-framework/types"
11+
)
12+
13+
type AttributeChanges struct {
14+
Changes []string
15+
}
16+
17+
func (a *AttributeChanges) LeafChanges() map[string]bool {
18+
return a.leafChanges(true)
19+
}
20+
21+
func (a *AttributeChanges) AttributeChanged(name string) bool {
22+
changes := a.LeafChanges()
23+
changed := changes[name]
24+
return changed
25+
}
26+
27+
func (a *AttributeChanges) KeepUnknown(attributeEffectedMapping map[string][]string) []string {
28+
var keepUnknown []string
29+
for attrChanged, affectedAttributes := range attributeEffectedMapping {
30+
if a.AttributeChanged(attrChanged) {
31+
keepUnknown = append(keepUnknown, attrChanged)
32+
keepUnknown = append(keepUnknown, affectedAttributes...)
33+
}
34+
}
35+
return keepUnknown
36+
}
37+
38+
// ListIndexChanged returns true if the list at the given index has changed, false if it was added or removed
39+
func (a *AttributeChanges) ListIndexChanged(name string, index int) bool {
40+
leafChanges := a.leafChanges(false)
41+
indexPath := fmt.Sprintf("%s[%d]", name, index)
42+
return leafChanges[indexPath]
43+
}
44+
45+
// NestedListLenChanges accepts a fullPath, e.g., "replication_specs[0].region_configs" and returns true if the length of the nested list has changed
46+
func (a *AttributeChanges) NestedListLenChanges(fullPath string) bool {
47+
addPrefix := fmt.Sprintf("%s[+", fullPath)
48+
removePrefix := fmt.Sprintf("%s[-", fullPath)
49+
for _, change := range a.Changes {
50+
if strings.HasPrefix(change, addPrefix) || strings.HasPrefix(change, removePrefix) {
51+
return true
52+
}
53+
}
54+
return false
55+
}
56+
57+
func (a *AttributeChanges) ListLenChanges(name string) bool {
58+
leafChanges := a.leafChanges(false)
59+
addPrefix := fmt.Sprintf("%s[+", name)
60+
removePrefix := fmt.Sprintf("%s[-", name)
61+
for change := range leafChanges {
62+
if strings.HasPrefix(change, addPrefix) || strings.HasPrefix(change, removePrefix) {
63+
return true
64+
}
65+
}
66+
return false
67+
}
68+
69+
func (a *AttributeChanges) leafChanges(removeIndex bool) map[string]bool {
70+
leafChanges := map[string]bool{}
71+
for _, change := range a.Changes {
72+
var leaf string
73+
parts := strings.Split(change, ".")
74+
leaf = parts[len(parts)-1]
75+
if removeIndex && strings.HasSuffix(leaf, "]") {
76+
leaf = strings.Split(leaf, "[")[0]
77+
}
78+
leafChanges[leaf] = true
79+
}
80+
return leafChanges
81+
}
82+
83+
// FindAttributeChanges: Iterates through TFModel of state+plan and returns AttributeChanges for querying changed attributes
84+
// The implementation is similar to KeepUnknown, no support for types.Set or types.Tuple yet
85+
func FindAttributeChanges(ctx context.Context, src, dest any) AttributeChanges {
86+
changes := FindChanges(ctx, src, dest)
87+
return AttributeChanges{changes}
88+
}
89+
90+
func FindChanges(ctx context.Context, src, dest any) []string {
91+
valSrc, valDest := validateStructPointers(src, dest)
92+
typeDest := valDest.Type()
93+
changes := []string{} // Always return an empty list, as nested attributes might be added and then removed, which make the test cases fail on nil vs []
94+
for i := range typeDest.NumField() {
95+
fieldDest := typeDest.Field(i)
96+
name, tfName := fieldNameTFName(&fieldDest)
97+
nestedSrc := valSrc.FieldByName(name).Interface()
98+
nestedDest := valDest.FieldByName(name).Interface()
99+
compareSrc := nestedSrc.(attr.Value)
100+
compareDest := nestedDest.(attr.Value)
101+
if compareDest.IsNull() || compareDest.IsUnknown() || compareDest.Equal(compareSrc) {
102+
continue
103+
}
104+
changes = append(changes, tfName)
105+
objValueSrc, okSrc := nestedSrc.(types.Object)
106+
objValueDest, okDest := nestedDest.(types.Object)
107+
if okSrc && okDest {
108+
moreChanges := findChangesInObject(ctx, objValueSrc, objValueDest, []string{tfName})
109+
if len(moreChanges) == 0 {
110+
changes = slices.Delete(changes, len(changes)-1, len(changes))
111+
}
112+
changes = append(changes, moreChanges...)
113+
continue
114+
}
115+
listValueSrc, okSrc := nestedSrc.(types.List)
116+
listValueDest, okDest := nestedDest.(types.List)
117+
if okSrc && okDest {
118+
moreChanges := findChangesInList(ctx, listValueSrc, listValueDest, []string{tfName})
119+
if len(moreChanges) == 0 {
120+
changes = slices.Delete(changes, len(changes)-1, len(changes))
121+
}
122+
changes = append(changes, moreChanges...)
123+
continue
124+
}
125+
}
126+
return changes
127+
}
128+
129+
func findChangesInObject(ctx context.Context, src, dest types.Object, parentPath []string) []string {
130+
var changes []string
131+
attributesSrc := src.Attributes()
132+
attributesDest := dest.Attributes()
133+
for name, attr := range attributesDest {
134+
path := slices.Clone(parentPath)
135+
path = append(path, name)
136+
if attr.IsNull() || attr.IsUnknown() || attr.Equal(attributesSrc[name]) {
137+
continue
138+
}
139+
changes = append(changes, strings.Join(path, "."))
140+
tfListDest, isList := attr.(types.List)
141+
tfObjectDest, isObject := attr.(types.Object)
142+
if isObject {
143+
moreChanges := findChangesInObject(ctx, attributesSrc[name].(types.Object), tfObjectDest, path)
144+
if len(moreChanges) == 0 {
145+
changes = slices.Delete(changes, len(changes)-1, len(changes))
146+
}
147+
changes = append(changes, moreChanges...)
148+
}
149+
if isList {
150+
moreChanges := findChangesInList(ctx, attributesSrc[name].(types.List), tfListDest, path)
151+
if len(moreChanges) == 0 {
152+
changes = slices.Delete(changes, len(changes)-1, len(changes))
153+
}
154+
changes = append(changes, moreChanges...)
155+
}
156+
}
157+
return changes
158+
}
159+
160+
func findChangesInList(ctx context.Context, src, dest types.List, parentPath []string) []string {
161+
changes := []string{}
162+
srcElements := src.Elements()
163+
destElements := dest.Elements()
164+
if dest.IsNull() {
165+
return changes
166+
}
167+
maxCount := max(len(srcElements), len(destElements))
168+
for i := range maxCount {
169+
srcObj, srcOk := lookupIndex(srcElements, i)
170+
destObj, destOk := lookupIndex(destElements, i)
171+
path := slices.Clone(parentPath)
172+
indexPath := fmt.Sprintf("%s[%d]", strings.Join(path, "."), i)
173+
switch {
174+
case srcOk && destOk:
175+
indexChanges := findChangesInObject(ctx, srcObj, destObj, []string{})
176+
if len(indexChanges) == 0 {
177+
continue
178+
}
179+
changes = append(changes, indexPath)
180+
for _, change := range indexChanges {
181+
changes = append(changes, fmt.Sprintf("%s.%s", indexPath, change))
182+
}
183+
case srcOk && !destOk: // removed from list
184+
changes = append(changes, fmt.Sprintf("%s[-%d]", strings.Join(parentPath, "."), i))
185+
default: // added to list
186+
changes = append(changes, fmt.Sprintf("%s[+%d]", strings.Join(parentPath, "."), i))
187+
}
188+
}
189+
return changes
190+
}
191+
func lookupIndex(elements []attr.Value, index int) (types.Object, bool) {
192+
if index >= len(elements) {
193+
return types.ObjectNull(nil), false
194+
}
195+
obj, ok := elements[index].(types.Object)
196+
return obj, ok
197+
}

0 commit comments

Comments
 (0)