Skip to content

Commit bbb5f5c

Browse files
Refactor detailed diff v2 to short circuit on nulls and unknowns (#2496)
This PR changes the logic in the detailed diff v2 calculation to short-circuit on encountering nils and unknowns. Previously, when we encountered a null or unknown in either news or olds we would still recurse down and compare the non-nil values versus a nil value in order to get any replacements which might be happening. We would then simplify the diff later to trim these extra diffs but would propagate the replacement to the right level. We would now instead short-circuit on encountering a null or unknown and walk the non-null value to find any properties which can trigger a replacement. This makes it much easier to handle sets in #2451 as recursing into sets is not necessary, as they are only compared by hashes. This change is not meant to change any behaviour and is tested in #2405 Stacked on #2515 and #2516
1 parent a38589e commit bbb5f5c

File tree

2 files changed

+249
-112
lines changed

2 files changed

+249
-112
lines changed

pkg/tfbridge/detailed_diff.go

Lines changed: 128 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/info"
1313
shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim"
1414
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/walk"
15+
"github.com/pulumi/pulumi-terraform-bridge/v3/unstable/propertyvalue"
1516
)
1617

1718
func isPresent(val resource.PropertyValue) bool {
@@ -41,6 +42,87 @@ func sortedMergedKeys[K cmp.Ordered, V any, M ~map[K]V](a, b M) []K {
4142
return keysSlice
4243
}
4344

45+
func isTypeShapeMismatched(val resource.PropertyValue, propType shim.ValueType) bool {
46+
contract.Assertf(!val.IsSecret() || val.IsOutput(), "secrets and outputs are not handled")
47+
if val.IsComputed() {
48+
return false
49+
}
50+
if !isPresent(val) {
51+
return false
52+
}
53+
switch propType {
54+
case shim.TypeList:
55+
return !val.IsArray()
56+
case shim.TypeSet:
57+
return !val.IsArray()
58+
case shim.TypeMap:
59+
return !val.IsObject()
60+
default:
61+
return false
62+
}
63+
}
64+
65+
func lookupSchemas(
66+
path propertyPath, tfs shim.SchemaMap, ps map[string]*info.Schema,
67+
) (shim.Schema, *info.Schema, error) {
68+
schemaPath := PropertyPathToSchemaPath(resource.PropertyPath(path), tfs, ps)
69+
return LookupSchemas(schemaPath, tfs, ps)
70+
}
71+
72+
func propertyPathTriggersReplacement(
73+
path propertyPath, rootTFSchema shim.SchemaMap, rootPulumiSchema map[string]*info.Schema,
74+
) bool {
75+
// A change on a property might trigger a replacement if:
76+
// - The property itself is marked as ForceNew
77+
// - The direct parent property is a collection (list, set, map) and is marked as ForceNew
78+
// See pkg/cross-tests/diff_cross_test.go
79+
// TestAttributeCollectionForceNew, TestBlockCollectionForceNew, TestBlockCollectionElementForceNew
80+
// for a full case study of replacements in TF
81+
tfs, ps, err := lookupSchemas(path, rootTFSchema, rootPulumiSchema)
82+
if err != nil {
83+
return false
84+
}
85+
if isForceNew(tfs, ps) {
86+
return true
87+
}
88+
89+
if len(path) == 1 {
90+
return false
91+
}
92+
93+
parent := path[:len(path)-1]
94+
tfs, ps, err = lookupSchemas(parent, rootTFSchema, rootPulumiSchema)
95+
if err != nil {
96+
return false
97+
}
98+
// Note this is mimicking the TF behaviour, so the effective type is not considered here.
99+
if tfs.Type() != shim.TypeList && tfs.Type() != shim.TypeSet && tfs.Type() != shim.TypeMap {
100+
return false
101+
}
102+
return isForceNew(tfs, ps)
103+
}
104+
105+
func propertyValueTriggersReplacement(
106+
path propertyPath, value resource.PropertyValue, tfs shim.SchemaMap, ps map[string]*info.Schema,
107+
) bool {
108+
replacement := false
109+
visitor := func(subpath resource.PropertyPath, val resource.PropertyValue) (resource.PropertyValue, error) {
110+
if propertyPathTriggersReplacement(propertyPath(subpath), tfs, ps) {
111+
replacement = true
112+
}
113+
return val, nil
114+
}
115+
116+
_, err := propertyvalue.TransformPropertyValue(
117+
resource.PropertyPath(path),
118+
visitor,
119+
value,
120+
)
121+
contract.AssertNoErrorf(err, "TransformPropertyValue should not return an error")
122+
123+
return replacement
124+
}
125+
44126
func promoteToReplace(diff *pulumirpc.PropertyDiff) *pulumirpc.PropertyDiff {
45127
if diff == nil {
46128
return nil
@@ -143,17 +225,6 @@ func (k propertyPath) IsReservedKey() bool {
143225
return leaf == "__meta" || leaf == "__defaults"
144226
}
145227

146-
func mapHasReplacements(m map[detailedDiffKey]*pulumirpc.PropertyDiff) bool {
147-
for _, diff := range m {
148-
if diff.GetKind() == pulumirpc.PropertyDiff_ADD_REPLACE ||
149-
diff.GetKind() == pulumirpc.PropertyDiff_DELETE_REPLACE ||
150-
diff.GetKind() == pulumirpc.PropertyDiff_UPDATE_REPLACE {
151-
return true
152-
}
153-
}
154-
return false
155-
}
156-
157228
type detailedDiffer struct {
158229
tfs shim.SchemaMap
159230
ps map[string]*SchemaInfo
@@ -182,79 +253,21 @@ func (differ detailedDiffer) getEffectiveType(path walk.SchemaPath) shim.ValueTy
182253
return tfs.Type()
183254
}
184255

185-
func (differ detailedDiffer) lookupSchemas(path propertyPath) (shim.Schema, *info.Schema, error) {
186-
schemaPath := PropertyPathToSchemaPath(resource.PropertyPath(path), differ.tfs, differ.ps)
187-
return LookupSchemas(schemaPath, differ.tfs, differ.ps)
188-
}
189-
190-
func (differ detailedDiffer) isForceNew(pair propertyPath) bool {
191-
// A change on a property might trigger a replacement if:
192-
// - The property itself is marked as ForceNew
193-
// - The direct parent property is a collection (list, set, map) and is marked as ForceNew
194-
// See pkg/cross-tests/diff_cross_test.go
195-
// TestAttributeCollectionForceNew, TestBlockCollectionForceNew, TestBlockCollectionElementForceNew
196-
// for a full case study of replacements in TF
197-
tfs, ps, err := differ.lookupSchemas(pair)
198-
if err != nil {
199-
return false
200-
}
201-
if isForceNew(tfs, ps) {
202-
return true
203-
}
204-
205-
if len(pair) == 1 {
206-
return false
207-
}
208-
209-
parent := pair[:len(pair)-1]
210-
tfs, ps, err = differ.lookupSchemas(parent)
211-
if err != nil {
212-
return false
213-
}
214-
// Note this is mimicking the TF behaviour, so the effective type is not considered here.
215-
if tfs.Type() != shim.TypeList && tfs.Type() != shim.TypeSet && tfs.Type() != shim.TypeMap {
216-
return false
217-
}
218-
return isForceNew(tfs, ps)
219-
}
220-
221-
// We do not short-circuit detailed diffs when comparing non-nil properties against nil ones. The reason for that is
222-
// that a replace might be triggered by a ForceNew inside a nested property of a non-ForceNew property. We instead
223-
// always walk the full tree even when comparing against a nil property. We then later do a simplification step for
224-
// the detailed diff in simplifyDiff in order to reduce the diff to what the user expects to see.
225-
// See [pulumi/pulumi-terraform-bridge#2405] for more details.
226-
func (differ detailedDiffer) simplifyDiff(
227-
diff map[detailedDiffKey]*pulumirpc.PropertyDiff, path propertyPath, old, new resource.PropertyValue,
228-
) (map[detailedDiffKey]*pulumirpc.PropertyDiff, bool) {
229-
baseDiff := makeBaseDiff(old, new)
230-
if baseDiff == undecidedDiff {
231-
return nil, false
232-
}
233-
propDiff := baseDiff.ToPropertyDiff()
234-
if propDiff == nil {
235-
return nil, true
236-
}
237-
if differ.isForceNew(path) || mapHasReplacements(diff) {
238-
propDiff = promoteToReplace(propDiff)
239-
}
240-
return map[detailedDiffKey]*pulumirpc.PropertyDiff{path.Key(): propDiff}, true
241-
}
242-
243256
// makePlainPropDiff is used for plain properties and ones with an unknown schema.
244257
// It does not access the TF schema, so it does not know about the type of the property.
245258
func (differ detailedDiffer) makePlainPropDiff(
246259
path propertyPath, old, new resource.PropertyValue,
247260
) map[detailedDiffKey]*pulumirpc.PropertyDiff {
248261
baseDiff := makeBaseDiff(old, new)
249-
isForceNew := differ.isForceNew(path)
262+
isReplacement := propertyPathTriggersReplacement(path, differ.tfs, differ.ps)
250263
var propDiff *pulumirpc.PropertyDiff
251264
if baseDiff != undecidedDiff {
252265
propDiff = baseDiff.ToPropertyDiff()
253266
} else if !old.DeepEquals(new) {
254267
propDiff = &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_UPDATE}
255268
}
256269

257-
if isForceNew {
270+
if isReplacement {
258271
propDiff = promoteToReplace(propDiff)
259272
}
260273

@@ -264,6 +277,32 @@ func (differ detailedDiffer) makePlainPropDiff(
264277
return nil
265278
}
266279

280+
// makeShortCircuitDiff is used for properties that are nil or computed in either the old or new state.
281+
// It makes sure to check recursively if the property will trigger a replacement.
282+
func (differ detailedDiffer) makeShortCircuitDiff(
283+
path propertyPath, old, new resource.PropertyValue,
284+
) map[detailedDiffKey]*pulumirpc.PropertyDiff {
285+
contract.Assertf(old.IsNull() || new.IsNull() || new.IsComputed(),
286+
"short-circuit diff should only be used for nil properties")
287+
if old.IsNull() && new.IsNull() {
288+
return nil
289+
}
290+
291+
baseDiff := makeBaseDiff(old, new)
292+
contract.Assertf(baseDiff != undecidedDiff, "short-circuit diff could not determine diff kind")
293+
294+
propDiff := baseDiff.ToPropertyDiff()
295+
if new.IsComputed() && propertyPathTriggersReplacement(path, differ.tfs, differ.ps) {
296+
propDiff = promoteToReplace(propDiff)
297+
} else if !new.IsNull() && !new.IsComputed() && propertyValueTriggersReplacement(path, new, differ.tfs, differ.ps) {
298+
propDiff = promoteToReplace(propDiff)
299+
} else if !old.IsNull() && propertyValueTriggersReplacement(path, old, differ.tfs, differ.ps) {
300+
propDiff = promoteToReplace(propDiff)
301+
}
302+
303+
return map[detailedDiffKey]*pulumirpc.PropertyDiff{path.Key(): propDiff}
304+
}
305+
267306
func (differ detailedDiffer) makePropDiff(
268307
path propertyPath, old, new resource.PropertyValue,
269308
) map[detailedDiffKey]*pulumirpc.PropertyDiff {
@@ -272,6 +311,20 @@ func (differ detailedDiffer) makePropDiff(
272311
}
273312
propType := differ.getEffectiveType(differ.propertyPathToSchemaPath(path))
274313

314+
if isTypeShapeMismatched(old, propType) || isTypeShapeMismatched(new, propType) {
315+
return differ.makePlainPropDiff(path, old, new)
316+
}
317+
318+
if !isPresent(old) {
319+
old = resource.NewNullProperty()
320+
}
321+
if !new.IsComputed() && !isPresent(new) {
322+
new = resource.NewNullProperty()
323+
}
324+
if old.IsNull() || new.IsNull() || new.IsComputed() {
325+
return differ.makeShortCircuitDiff(path, old, new)
326+
}
327+
275328
switch propType {
276329
case shim.TypeList:
277330
return differ.makeListDiff(path, old, new)
@@ -290,14 +343,8 @@ func (differ detailedDiffer) makeListDiff(
290343
path propertyPath, old, new resource.PropertyValue,
291344
) map[detailedDiffKey]*pulumirpc.PropertyDiff {
292345
diff := make(map[detailedDiffKey]*pulumirpc.PropertyDiff)
293-
oldList := []resource.PropertyValue{}
294-
newList := []resource.PropertyValue{}
295-
if isPresent(old) && old.IsArray() {
296-
oldList = old.ArrayValue()
297-
}
298-
if isPresent(new) && new.IsArray() {
299-
newList = new.ArrayValue()
300-
}
346+
oldList := old.ArrayValue()
347+
newList := new.ArrayValue()
301348

302349
// naive diffing of lists
303350
// TODO[pulumi/pulumi-terraform-bridge#2295]: implement a more sophisticated diffing algorithm
@@ -318,27 +365,15 @@ func (differ detailedDiffer) makeListDiff(
318365
}
319366
}
320367

321-
simplerDiff, isSimplified := differ.simplifyDiff(diff, path, old, new)
322-
if isSimplified {
323-
return simplerDiff
324-
}
325-
326368
return diff
327369
}
328370

329371
func (differ detailedDiffer) makeMapDiff(
330372
path propertyPath, old, new resource.PropertyValue,
331373
) map[detailedDiffKey]*pulumirpc.PropertyDiff {
374+
oldMap := old.ObjectValue()
375+
newMap := new.ObjectValue()
332376
diff := make(map[detailedDiffKey]*pulumirpc.PropertyDiff)
333-
oldMap := resource.PropertyMap{}
334-
newMap := resource.PropertyMap{}
335-
if isPresent(old) && old.IsObject() {
336-
oldMap = old.ObjectValue()
337-
}
338-
if isPresent(new) && new.IsObject() {
339-
newMap = new.ObjectValue()
340-
}
341-
342377
for _, k := range sortedMergedKeys(oldMap, newMap) {
343378
subindex := path.Subpath(string(k))
344379
oldVal := oldMap[k]
@@ -351,11 +386,6 @@ func (differ detailedDiffer) makeMapDiff(
351386
}
352387
}
353388

354-
simplerDiff, isSimplified := differ.simplifyDiff(diff, path, old, new)
355-
if isSimplified {
356-
return simplerDiff
357-
}
358-
359389
return diff
360390
}
361391

0 commit comments

Comments
 (0)