7 "github.com/zclconf/go-cty/cty"
8 "github.com/zclconf/go-cty/cty/convert"
10 "github.com/hashicorp/terraform/configs/configschema"
13 // AssertObjectCompatible checks whether the given "actual" value is a valid
14 // completion of the possibly-partially-unknown "planned" value.
16 // This means that any known leaf value in "planned" must be equal to the
17 // corresponding value in "actual", and various other similar constraints.
19 // Any inconsistencies are reported by returning a non-zero number of errors.
20 // These errors are usually (but not necessarily) cty.PathError values
21 // referring to a particular nested value within the "actual" value.
23 // The two values must have types that conform to the given schema's implied
24 // type, or this function will panic.
25 func AssertObjectCompatible(schema *configschema.Block, planned, actual cty.Value) []error {
26 return assertObjectCompatible(schema, planned, actual, nil)
29 func assertObjectCompatible(schema *configschema.Block, planned, actual cty.Value, path cty.Path) []error {
31 if planned.IsNull() && !actual.IsNull() {
32 errs = append(errs, path.NewErrorf("was absent, but now present"))
35 if actual.IsNull() && !planned.IsNull() {
36 errs = append(errs, path.NewErrorf("was present, but now absent"))
40 // No further checks possible if both values are null
44 for name, attrS := range schema.Attributes {
45 plannedV := planned.GetAttr(name)
46 actualV := actual.GetAttr(name)
48 path := append(path, cty.GetAttrStep{Name: name})
49 moreErrs := assertValueCompatible(plannedV, actualV, path)
51 if len(moreErrs) > 0 {
52 // Use a vague placeholder message instead, to avoid disclosing
53 // sensitive information.
54 errs = append(errs, path.NewErrorf("inconsistent values for sensitive attribute"))
57 errs = append(errs, moreErrs...)
60 for name, blockS := range schema.BlockTypes {
61 plannedV := planned.GetAttr(name)
62 actualV := actual.GetAttr(name)
64 // As a special case, if there were any blocks whose leaf attributes
65 // are all unknown then we assume (possibly incorrectly) that the
66 // HCL dynamic block extension is in use with an unknown for_each
67 // argument, and so we will do looser validation here that allows
68 // for those blocks to have expanded into a different number of blocks
69 // if the for_each value is now known.
70 maybeUnknownBlocks := couldHaveUnknownBlockPlaceholder(plannedV, blockS, false)
72 path := append(path, cty.GetAttrStep{Name: name})
73 switch blockS.Nesting {
74 case configschema.NestingSingle, configschema.NestingGroup:
75 // If an unknown block placeholder was present then the placeholder
76 // may have expanded out into zero blocks, which is okay.
77 if maybeUnknownBlocks && actualV.IsNull() {
80 moreErrs := assertObjectCompatible(&blockS.Block, plannedV, actualV, path)
81 errs = append(errs, moreErrs...)
82 case configschema.NestingList:
83 // A NestingList might either be a list or a tuple, depending on
84 // whether there are dynamically-typed attributes inside. However,
85 // both support a similar-enough API that we can treat them the
86 // same for our purposes here.
87 if !plannedV.IsKnown() || plannedV.IsNull() || actualV.IsNull() {
91 if maybeUnknownBlocks {
92 // When unknown blocks are present the final blocks may be
93 // at different indices than the planned blocks, so unfortunately
94 // we can't do our usual checks in this case without generating
99 plannedL := plannedV.LengthInt()
100 actualL := actualV.LengthInt()
101 if plannedL != actualL {
102 errs = append(errs, path.NewErrorf("block count changed from %d to %d", plannedL, actualL))
105 for it := plannedV.ElementIterator(); it.Next(); {
106 idx, plannedEV := it.Element()
107 if !actualV.HasIndex(idx).True() {
110 actualEV := actualV.Index(idx)
111 moreErrs := assertObjectCompatible(&blockS.Block, plannedEV, actualEV, append(path, cty.IndexStep{Key: idx}))
112 errs = append(errs, moreErrs...)
114 case configschema.NestingMap:
115 // A NestingMap might either be a map or an object, depending on
116 // whether there are dynamically-typed attributes inside, but
117 // that's decided statically and so both values will have the same
119 if plannedV.Type().IsObjectType() {
120 plannedAtys := plannedV.Type().AttributeTypes()
121 actualAtys := actualV.Type().AttributeTypes()
122 for k := range plannedAtys {
123 if _, ok := actualAtys[k]; !ok {
124 errs = append(errs, path.NewErrorf("block key %q has vanished", k))
128 plannedEV := plannedV.GetAttr(k)
129 actualEV := actualV.GetAttr(k)
130 moreErrs := assertObjectCompatible(&blockS.Block, plannedEV, actualEV, append(path, cty.GetAttrStep{Name: k}))
131 errs = append(errs, moreErrs...)
133 if !maybeUnknownBlocks { // new blocks may appear if unknown blocks were present in the plan
134 for k := range actualAtys {
135 if _, ok := plannedAtys[k]; !ok {
136 errs = append(errs, path.NewErrorf("new block key %q has appeared", k))
142 if !plannedV.IsKnown() || plannedV.IsNull() || actualV.IsNull() {
145 plannedL := plannedV.LengthInt()
146 actualL := actualV.LengthInt()
147 if plannedL != actualL && !maybeUnknownBlocks { // new blocks may appear if unknown blocks were persent in the plan
148 errs = append(errs, path.NewErrorf("block count changed from %d to %d", plannedL, actualL))
151 for it := plannedV.ElementIterator(); it.Next(); {
152 idx, plannedEV := it.Element()
153 if !actualV.HasIndex(idx).True() {
156 actualEV := actualV.Index(idx)
157 moreErrs := assertObjectCompatible(&blockS.Block, plannedEV, actualEV, append(path, cty.IndexStep{Key: idx}))
158 errs = append(errs, moreErrs...)
161 case configschema.NestingSet:
162 if !plannedV.IsKnown() || !actualV.IsKnown() || plannedV.IsNull() || actualV.IsNull() {
166 setErrs := assertSetValuesCompatible(plannedV, actualV, path, func(plannedEV, actualEV cty.Value) bool {
167 errs := assertObjectCompatible(&blockS.Block, plannedEV, actualEV, append(path, cty.IndexStep{Key: actualEV}))
168 return len(errs) == 0
170 errs = append(errs, setErrs...)
172 // There can be fewer elements in a set after its elements are all
173 // known (values that turn out to be equal will coalesce) but the
174 // number of elements must never get larger.
175 plannedL := plannedV.LengthInt()
176 actualL := actualV.LengthInt()
177 if plannedL < actualL {
178 errs = append(errs, path.NewErrorf("block set length changed from %d to %d", plannedL, actualL))
181 panic(fmt.Sprintf("unsupported nesting mode %s", blockS.Nesting))
187 func assertValueCompatible(planned, actual cty.Value, path cty.Path) []error {
188 // NOTE: We don't normally use the GoString rendering of cty.Value in
189 // user-facing error messages as a rule, but we make an exception
190 // for this function because we expect the user to pass this message on
191 // verbatim to the provider development team and so more detail is better.
194 if planned.Type() == cty.DynamicPseudoType {
195 // Anything goes, then
198 if problems := planned.Type().TestConformance(actual.Type()); len(problems) > 0 {
199 errs = append(errs, path.NewErrorf("wrong final value type: %s", convert.MismatchMessage(actual.Type(), planned.Type())))
200 // If the types don't match then we can't do any other comparisons,
205 if !planned.IsKnown() {
206 // We didn't know what were going to end up with during plan, so
207 // anything goes during apply.
212 if planned.IsNull() {
215 errs = append(errs, path.NewErrorf("was %#v, but now null", planned))
218 if planned.IsNull() {
219 errs = append(errs, path.NewErrorf("was null, but now %#v", actual))
226 case !actual.IsKnown():
227 errs = append(errs, path.NewErrorf("was known, but now unknown"))
229 case ty.IsPrimitiveType():
230 if !actual.Equals(planned).True() {
231 errs = append(errs, path.NewErrorf("was %#v, but now %#v", planned, actual))
234 case ty.IsListType() || ty.IsMapType() || ty.IsTupleType():
235 for it := planned.ElementIterator(); it.Next(); {
236 k, plannedV := it.Element()
237 if !actual.HasIndex(k).True() {
238 errs = append(errs, path.NewErrorf("element %s has vanished", indexStrForErrors(k)))
242 actualV := actual.Index(k)
243 moreErrs := assertValueCompatible(plannedV, actualV, append(path, cty.IndexStep{Key: k}))
244 errs = append(errs, moreErrs...)
247 for it := actual.ElementIterator(); it.Next(); {
249 if !planned.HasIndex(k).True() {
250 errs = append(errs, path.NewErrorf("new element %s has appeared", indexStrForErrors(k)))
254 case ty.IsObjectType():
255 atys := ty.AttributeTypes()
256 for name := range atys {
257 // Because we already tested that the two values have the same type,
258 // we can assume that the same attributes are present in both and
259 // focus just on testing their values.
260 plannedV := planned.GetAttr(name)
261 actualV := actual.GetAttr(name)
262 moreErrs := assertValueCompatible(plannedV, actualV, append(path, cty.GetAttrStep{Name: name}))
263 errs = append(errs, moreErrs...)
267 // We can't really do anything useful for sets here because changing
268 // an unknown element to known changes the identity of the element, and
269 // so we can't correlate them properly. However, we will at least check
270 // to ensure that the number of elements is consistent, along with
271 // the general type-match checks we ran earlier in this function.
272 if planned.IsKnown() && !planned.IsNull() && !actual.IsNull() {
274 setErrs := assertSetValuesCompatible(planned, actual, path, func(plannedV, actualV cty.Value) bool {
275 errs := assertValueCompatible(plannedV, actualV, append(path, cty.IndexStep{Key: actualV}))
276 return len(errs) == 0
278 errs = append(errs, setErrs...)
280 // There can be fewer elements in a set after its elements are all
281 // known (values that turn out to be equal will coalesce) but the
282 // number of elements must never get larger.
284 plannedL := planned.LengthInt()
285 actualL := actual.LengthInt()
286 if plannedL < actualL {
287 errs = append(errs, path.NewErrorf("length changed from %d to %d", plannedL, actualL))
295 func indexStrForErrors(v cty.Value) string {
298 return v.AsBigFloat().Text('f', -1)
300 return strconv.Quote(v.AsString())
302 // Should be impossible, since no other index types are allowed!
303 return fmt.Sprintf("%#v", v)
307 // couldHaveUnknownBlockPlaceholder is a heuristic that recognizes how the
308 // HCL dynamic block extension behaves when it's asked to expand a block whose
309 // for_each argument is unknown. In such cases, it generates a single placeholder
310 // block with all leaf attribute values unknown, and once the for_each
311 // expression becomes known the placeholder may be replaced with any number
312 // of blocks, so object compatibility checks would need to be more liberal.
314 // Set "nested" if testing a block that is nested inside a candidate block
315 // placeholder; this changes the interpretation of there being no blocks of
316 // a type to allow for there being zero nested blocks.
317 func couldHaveUnknownBlockPlaceholder(v cty.Value, blockS *configschema.NestedBlock, nested bool) bool {
318 switch blockS.Nesting {
319 case configschema.NestingSingle, configschema.NestingGroup:
320 if nested && v.IsNull() {
321 return true // for nested blocks, a single block being unset doesn't disqualify from being an unknown block placeholder
323 return couldBeUnknownBlockPlaceholderElement(v, &blockS.Block)
325 // These situations should be impossible for correct providers, but
326 // we permit the legacy SDK to produce some incorrect outcomes
327 // for compatibility with its existing logic, and so we must be
333 return false // treated as if the list were empty, so we would see zero iterations below
336 // For all other nesting modes, our value should be something iterable.
337 for it := v.ElementIterator(); it.Next(); {
338 _, ev := it.Element()
339 if couldBeUnknownBlockPlaceholderElement(ev, &blockS.Block) {
344 // Our default changes depending on whether we're testing the candidate
345 // block itself or something nested inside of it: zero blocks of a type
346 // can never contain a dynamic block placeholder, but a dynamic block
347 // placeholder might contain zero blocks of one of its own nested block
348 // types, if none were set in the config at all.
353 func couldBeUnknownBlockPlaceholderElement(v cty.Value, schema *configschema.Block) bool {
355 return false // null value can never be a placeholder element
358 return true // this should never happen for well-behaved providers, but can happen with the legacy SDK opt-outs
360 for name := range schema.Attributes {
361 av := v.GetAttr(name)
363 // Unknown block placeholders contain only unknown or null attribute
364 // values, depending on whether or not a particular attribute was set
365 // explicitly inside the content block. Note that this is imprecise:
366 // non-placeholders can also match this, so this function can generate
368 if av.IsKnown() && !av.IsNull() {
372 for name, blockS := range schema.BlockTypes {
373 if !couldHaveUnknownBlockPlaceholder(v.GetAttr(name), blockS, true) {
380 // assertSetValuesCompatible checks that each of the elements in a can
381 // be correlated with at least one equivalent element in b and vice-versa,
382 // using the given correlation function.
384 // This allows the number of elements in the sets to change as long as all
385 // elements in both sets can be correlated, making this function safe to use
386 // with sets that may contain unknown values as long as the unknown case is
387 // addressed in some reasonable way in the callback function.
389 // The callback always recieves values from set a as its first argument and
390 // values from set b in its second argument, so it is safe to use with
391 // non-commutative functions.
393 // As with assertValueCompatible, we assume that the target audience of error
394 // messages here is a provider developer (via a bug report from a user) and so
395 // we intentionally violate our usual rule of keeping cty implementation
396 // details out of error messages.
397 func assertSetValuesCompatible(planned, actual cty.Value, path cty.Path, f func(aVal, bVal cty.Value) bool) []error {
401 // Our methodology here is a little tricky, to deal with the fact that
402 // it's impossible to directly correlate two non-equal set elements because
403 // they don't have identities separate from their values.
404 // The approach is to count the number of equivalent elements each element
405 // of a has in b and vice-versa, and then return true only if each element
406 // in both sets has at least one equivalent.
407 as := a.AsValueSlice()
408 bs := b.AsValueSlice()
409 aeqs := make([]bool, len(as))
410 beqs := make([]bool, len(bs))
411 for ai, av := range as {
412 for bi, bv := range bs {
421 for i, eq := range aeqs {
423 errs = append(errs, path.NewErrorf("planned set element %#v does not correlate with any element in actual", as[i]))
427 // Exit early since otherwise we're likely to generate duplicate
428 // error messages from the other perspective in the subsequent loop.
431 for i, eq := range beqs {
433 errs = append(errs, path.NewErrorf("actual set element %#v does not correlate with any element in plan", bs[i]))