@@ -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
1718func 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+
44126func 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-
157228type 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.
245258func (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+
267306func (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
329371func (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