@@ -275,6 +275,14 @@ func proposedNewNestedBlock(schema *configschema.NestedBlock, prior, config cty.
275275 return newV
276276}
277277
278+ func proposedNewObjectAttributes (attrs map [string ]* configschema.Attribute , prior , config cty.Value ) cty.Value {
279+ if config .IsNull () {
280+ return config
281+ }
282+
283+ return cty .ObjectVal (proposedNewAttributes (attrs , prior , config ))
284+ }
285+
278286func proposedNewAttributes (attrs map [string ]* configschema.Attribute , prior , config cty.Value ) map [string ]cty.Value {
279287 newAttrs := make (map [string ]cty.Value , len (attrs ))
280288 for name , attr := range attrs {
@@ -297,6 +305,14 @@ func proposedNewAttributes(attrs map[string]*configschema.Attribute, prior, conf
297305 // configV will always be null in this case, by definition.
298306 // priorV may also be null, but that's okay.
299307 newV = priorV
308+
309+ // the exception to the above is that if the config is optional and
310+ // the _prior_ value contains non-computed values, we can infer
311+ // that the config must have been non-null previously.
312+ if optionalValueNotComputable (attr , priorV ) {
313+ newV = configV
314+ }
315+
300316 case attr .NestedType != nil :
301317 // For non-computed NestedType attributes, we need to descend
302318 // into the individual nested attributes to build the final
@@ -316,7 +332,7 @@ func proposedNewAttributes(attrs map[string]*configschema.Attribute, prior, conf
316332
317333func proposedNewNestedType (schema * configschema.Object , prior , config cty.Value ) cty.Value {
318334 // if the config isn't known at all, then we must use that value
319- if ! config .IsNull () && ! config . IsKnown () {
335+ if ! config .IsKnown () {
320336 return config
321337 }
322338
@@ -332,7 +348,7 @@ func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value)
332348 break
333349 }
334350
335- newV = cty . ObjectVal ( proposedNewAttributes ( schema .Attributes , prior , config ) )
351+ newV = proposedNewObjectAttributes ( schema .Attributes , prior , config )
336352
337353 case configschema .NestingList :
338354 // Nested blocks are correlated by index.
@@ -353,8 +369,8 @@ func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value)
353369 }
354370 priorEV := prior .Index (idx )
355371
356- newEV := proposedNewAttributes (schema .Attributes , priorEV , configEV )
357- newVals = append (newVals , cty . ObjectVal ( newEV ) )
372+ newEV := proposedNewObjectAttributes (schema .Attributes , priorEV , configEV )
373+ newVals = append (newVals , newEV )
358374 }
359375 // Despite the name, a NestingList might also be a tuple, if
360376 // its nested schema contains dynamically-typed attributes.
@@ -366,58 +382,55 @@ func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value)
366382 }
367383
368384 case configschema .NestingMap :
385+ configVLen := 0
386+ if config .IsKnown () && ! config .IsNull () {
387+ configVLen = config .LengthInt ()
388+ }
389+
390+ if configVLen == 0 {
391+ break
392+ }
393+
394+ newVals := make (map [string ]cty.Value , configVLen )
395+
369396 // Despite the name, a NestingMap may produce either a map or
370397 // object value, depending on whether the nested schema contains
371398 // dynamically-typed attributes.
372399 if config .Type ().IsObjectType () {
373400 // Nested blocks are correlated by key.
374- configVLen := 0
375- if config .IsKnown () && ! config .IsNull () {
376- configVLen = config .LengthInt ()
377- }
378- if configVLen > 0 {
379- newVals := make (map [string ]cty.Value , configVLen )
380- atys := config .Type ().AttributeTypes ()
381- for name := range atys {
382- configEV := config .GetAttr (name )
383- if ! prior .IsKnown () || prior .IsNull () || ! prior .Type ().HasAttribute (name ) {
384- // If there is no corresponding prior element then
385- // we just take the config value as-is.
386- newVals [name ] = configEV
387- continue
388- }
389- priorEV := prior .GetAttr (name )
390- newEV := proposedNewAttributes (schema .Attributes , priorEV , configEV )
391- newVals [name ] = cty .ObjectVal (newEV )
401+ atys := config .Type ().AttributeTypes ()
402+ for name := range atys {
403+ configEV := config .GetAttr (name )
404+ if ! prior .IsKnown () || prior .IsNull () || ! prior .Type ().HasAttribute (name ) {
405+ // If there is no corresponding prior element then
406+ // we just take the config value as-is.
407+ newVals [name ] = configEV
408+ continue
392409 }
393- // Although we call the nesting mode "map", we actually use
394- // object values so that elements might have different types
395- // in case of dynamically-typed attributes.
396- newV = cty .ObjectVal (newVals )
410+ priorEV := prior .GetAttr (name )
411+ newEV := proposedNewObjectAttributes (schema .Attributes , priorEV , configEV )
412+ newVals [name ] = newEV
397413 }
414+ // Although we call the nesting mode "map", we actually use
415+ // object values so that elements might have different types
416+ // in case of dynamically-typed attributes.
417+ newV = cty .ObjectVal (newVals )
398418 } else {
399- configVLen := 0
400- if config .IsKnown () && ! config .IsNull () {
401- configVLen = config .LengthInt ()
402- }
403- if configVLen > 0 {
404- newVals := make (map [string ]cty.Value , configVLen )
405- for it := config .ElementIterator (); it .Next (); {
406- idx , configEV := it .Element ()
407- k := idx .AsString ()
408- if prior .IsKnown () && (prior .IsNull () || ! prior .HasIndex (idx ).True ()) {
409- // If there is no corresponding prior element then
410- // we just take the config value as-is.
411- newVals [k ] = configEV
412- continue
413- }
414- priorEV := prior .Index (idx )
415-
416- newEV := proposedNewAttributes (schema .Attributes , priorEV , configEV )
417- newVals [k ] = cty .ObjectVal (newEV )
419+ for it := config .ElementIterator (); it .Next (); {
420+ idx , configEV := it .Element ()
421+ k := idx .AsString ()
422+ if prior .IsKnown () && (prior .IsNull () || ! prior .HasIndex (idx ).True ()) {
423+ // If there is no corresponding prior element then
424+ // we just take the config value as-is.
425+ newVals [k ] = configEV
426+ continue
418427 }
419- newV = cty .MapVal (newVals )
428+ priorEV := prior .Index (idx )
429+
430+ newEV := proposedNewObjectAttributes (schema .Attributes , priorEV , configEV )
431+ newVals [k ] = newEV
420432 }
433+ newV = cty .MapVal (newVals )
421434 }
422435
423436 case configschema .NestingSet :
@@ -453,8 +466,8 @@ func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value)
453466 if priorEV == cty .NilVal {
454467 newVals = append (newVals , configEV )
455468 } else {
456- newEV := proposedNewAttributes (schema .Attributes , priorEV , configEV )
457- newVals = append (newVals , cty . ObjectVal ( newEV ) )
469+ newEV := proposedNewObjectAttributes (schema .Attributes , priorEV , configEV )
470+ newVals = append (newVals , newEV )
458471 }
459472 }
460473 newV = cty .SetVal (newVals )
@@ -518,3 +531,43 @@ func setElementComputedAsNull(schema attrPath, elem cty.Value) cty.Value {
518531
519532 return elem
520533}
534+
535+ // optionalValueNotComputable is used to check if an object in state must
536+ // have at least partially come from configuration. If the prior value has any
537+ // non-null attributes which are not computed in the schema, then we know there
538+ // was previously a configuration value which set those.
539+ //
540+ // This is used when the configuration contains a null optional+computed value,
541+ // and we want to know if we should plan to send the null value or the prior
542+ // state.
543+ func optionalValueNotComputable (schema * configschema.Attribute , val cty.Value ) bool {
544+ if ! schema .Optional {
545+ return false
546+ }
547+
548+ // We must have a NestedType for complex nested attributes in order
549+ // to find nested computed values in the first place.
550+ if schema .NestedType == nil {
551+ return false
552+ }
553+
554+ foundNonComputedAttr := false
555+ cty .Walk (val , func (path cty.Path , v cty.Value ) (bool , error ) {
556+ if v .IsNull () {
557+ return true , nil
558+ }
559+
560+ attr := schema .NestedType .AttributeByPath (path )
561+ if attr == nil {
562+ return true , nil
563+ }
564+
565+ if ! attr .Computed {
566+ foundNonComputedAttr = true
567+ return false , nil
568+ }
569+ return true , nil
570+ })
571+
572+ return foundNonComputedAttr
573+ }
0 commit comments