Skip to content

Commit 7ef4e7f

Browse files
authored
Merge pull request #25857 from hashicorp/jbardin/data-diffs
allow plan data state comparison with legacy SDK
2 parents d844d02 + 93246bd commit 7ef4e7f

File tree

2 files changed

+445
-13
lines changed

2 files changed

+445
-13
lines changed

terraform/eval_read_data_plan.go

Lines changed: 104 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
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

Comments
 (0)