77
88 "github.com/zclconf/go-cty/cty"
99
10+ "github.com/hashicorp/terraform/configs/configschema"
1011 "github.com/hashicorp/terraform/plans"
1112 "github.com/hashicorp/terraform/plans/objchange"
1213 "github.com/hashicorp/terraform/states"
@@ -101,22 +102,18 @@ func (n *evalReadDataPlan) Eval(ctx EvalContext) (interface{}, error) {
101102 return nil , diags .ErrWithWarnings ()
102103 }
103104
104- var proposedVal cty.Value
105-
106105 // If we have a stored state we may not need to re-read the data source.
107106 // Check the config against the state to see if there are any difference.
108- if ! priorVal .IsNull () {
109- // Applying the configuration to the prior state lets us see if there
110- // are any differences.
111- proposedVal = objchange .ProposedNewObject (schema , priorVal , configVal )
112- if proposedVal .Equals (priorVal ).True () {
113- log .Printf ("[TRACE] evalReadDataPlan: %s no change detected, using existing state" , absAddr )
114- // state looks up to date, and must have been read during refresh
115- return nil , diags .ErrWithWarnings ()
116- }
117- log .Printf ("[TRACE] evalReadDataPlan: %s configuration changed, planning data source" , absAddr )
107+ proposedVal , hasChanges := dataObjectHasChanges (schema , priorVal , configVal )
108+
109+ if ! hasChanges {
110+ log .Printf ("[TRACE] evalReadDataPlan: %s no change detected, using existing state" , absAddr )
111+ // state looks up to date, and must have been read during refresh
112+ return nil , diags .ErrWithWarnings ()
118113 }
119114
115+ log .Printf ("[TRACE] evalReadDataPlan: %s configuration changed, planning data source" , absAddr )
116+
120117 newVal , readDiags := n .readDataSource (ctx , configVal )
121118 diags = diags .Append (readDiags )
122119 if diags .HasErrors () {
@@ -132,7 +129,7 @@ func (n *evalReadDataPlan) Eval(ctx EvalContext) (interface{}, error) {
132129 // compatible with the config+schema. Since we can't detect the legacy
133130 // type system, we can only warn about this for now.
134131 var buf strings.Builder
135- fmt .Fprintf (& buf , "[WARN] Provider %q produced an unexpected new value for %s." +
132+ fmt .Fprintf (& buf , "[WARN] Provider %q produced an unexpected new value for %s." ,
136133 n .ProviderAddr .Provider .String (), absAddr )
137134 for _ , err := range errs {
138135 fmt .Fprintf (& buf , "\n - %s" , tfdiags .FormatError (err ))
@@ -191,3 +188,97 @@ func (n *evalReadDataPlan) forcePlanRead(ctx EvalContext) bool {
191188 }
192189 return false
193190}
191+
192+ // dataObjectHasChanges determines if the newly evaluated config would cause
193+ // any changes in the stored value, indicating that we need to re-read this
194+ // data source. The proposed value is returned for validation against the
195+ // ReadDataSource response.
196+ func dataObjectHasChanges (schema * configschema.Block , priorVal , configVal cty.Value ) (proposedVal cty.Value , hasChanges bool ) {
197+ if priorVal .IsNull () {
198+ return priorVal , true
199+ }
200+
201+ // Applying the configuration to the stored state will allow us to detect any changes.
202+ proposedVal = objchange .ProposedNewObject (schema , priorVal , configVal )
203+
204+ if ! configVal .IsWhollyKnown () {
205+ // Config should have been known here, but handle it the same as ProposedNewObject
206+ return proposedVal , true
207+ }
208+
209+ // Normalize the prior value so we can correctly compare the two even if
210+ // the prior value came through the legacy SDK.
211+ priorVal = createEmptyBlocks (schema , priorVal )
212+
213+ return proposedVal , proposedVal .Equals (priorVal ).False ()
214+ }
215+
216+ // createEmptyBlocks will fill in null TypeList or TypeSet blocks with Empty
217+ // values. Our decoder will always decode blocks as empty containers, but the
218+ // legacy SDK may replace those will null values. Normalizing these values
219+ // allows us to correctly compare the ProposedNewObject value in
220+ // dataObjectyHasChanges.
221+ func createEmptyBlocks (schema * configschema.Block , val cty.Value ) cty.Value {
222+ if val .IsNull () || ! val .IsKnown () {
223+ return val
224+ }
225+ if ! val .Type ().IsObjectType () {
226+ panic (fmt .Sprintf ("unexpected type %#v\n " , val .Type ()))
227+ }
228+
229+ // if there are no blocks, don't bother recreating the cty.Value
230+ if len (schema .BlockTypes ) == 0 {
231+ return val
232+ }
233+
234+ objMap := val .AsValueMap ()
235+
236+ for name , blockType := range schema .BlockTypes {
237+ block , ok := objMap [name ]
238+ if ! ok {
239+ continue
240+ }
241+
242+ ety := block .Type ().ElementType ()
243+
244+ // helper to build the recursive block values
245+ nextBlocks := func () []cty.Value {
246+ // this is only called once we know this is a non-null List or Set
247+ // with a length > 0
248+ newVals := make ([]cty.Value , 0 , block .LengthInt ())
249+ for it := block .ElementIterator (); it .Next (); {
250+ _ , val := it .Element ()
251+ newVals = append (newVals , createEmptyBlocks (& blockType .Block , val ))
252+ }
253+ return newVals
254+ }
255+
256+ // Blocks are always decoded as empty containers, but the legacy
257+ // SDK may return null when they are empty.
258+ switch blockType .Nesting {
259+ // We are only concerned with block types that can come from the legacy
260+ // sdk, which means TypeList or TypeSet.
261+ case configschema .NestingList :
262+ switch {
263+ case block .IsNull ():
264+ objMap [name ] = cty .ListValEmpty (ety )
265+ case block .LengthInt () == 0 :
266+ continue
267+ default :
268+ objMap [name ] = cty .ListVal (nextBlocks ())
269+ }
270+
271+ case configschema .NestingSet :
272+ switch {
273+ case block .IsNull ():
274+ objMap [name ] = cty .SetValEmpty (ety )
275+ case block .LengthInt () == 0 :
276+ continue
277+ default :
278+ objMap [name ] = cty .SetVal (nextBlocks ())
279+ }
280+ }
281+ }
282+
283+ return cty .ObjectVal (objMap )
284+ }
0 commit comments