plans/objchange: AssertObjectSuperset function
This function's goal is to ensure that the "final" plan value produced by a provider during the apply step is always consistent with the known parts of the planned value produced during the plan step. Any error produced here indicates a bug in the provider.
This commit is contained in:
parent
0317da9911
commit
4c78539c2b
|
@ -0,0 +1,232 @@
|
|||
package objchange
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
|
||||
"github.com/hashicorp/terraform/configs/configschema"
|
||||
)
|
||||
|
||||
// AssertObjectCompatible checks whether the given "actual" value is a valid
|
||||
// completion of the possibly-partially-unknown "planned" value.
|
||||
//
|
||||
// This means that any known leaf value in "planned" must be equal to the
|
||||
// corresponding value in "actual", and various other similar constraints.
|
||||
//
|
||||
// Any inconsistencies are reported by returning a non-zero number of errors.
|
||||
// These errors are usually (but not necessarily) cty.PathError values
|
||||
// referring to a particular nested value within the "actual" value.
|
||||
//
|
||||
// The two values must have types that conform to the given schema's implied
|
||||
// type, or this function will panic.
|
||||
func AssertObjectCompatible(schema *configschema.Block, planned, actual cty.Value) []error {
|
||||
return assertObjectCompatible(schema, planned, actual, nil)
|
||||
}
|
||||
|
||||
func assertObjectCompatible(schema *configschema.Block, planned, actual cty.Value, path cty.Path) []error {
|
||||
var errs []error
|
||||
if planned.IsNull() && !actual.IsNull() {
|
||||
errs = append(errs, path.NewErrorf("was absent, but now present"))
|
||||
return errs
|
||||
}
|
||||
if actual.IsNull() && !planned.IsNull() {
|
||||
errs = append(errs, path.NewErrorf("was present, but now absent"))
|
||||
return errs
|
||||
}
|
||||
if planned.IsNull() {
|
||||
// No further checks possible if both values are null
|
||||
return errs
|
||||
}
|
||||
|
||||
for name := range schema.Attributes {
|
||||
plannedV := planned.GetAttr(name)
|
||||
actualV := actual.GetAttr(name)
|
||||
|
||||
path := append(path, cty.GetAttrStep{Name: name})
|
||||
moreErrs := assertValueCompatible(plannedV, actualV, path)
|
||||
errs = append(errs, moreErrs...)
|
||||
}
|
||||
for name, blockS := range schema.BlockTypes {
|
||||
plannedV := planned.GetAttr(name)
|
||||
actualV := actual.GetAttr(name)
|
||||
|
||||
// As a special case, we permit a "planned" block with exactly one
|
||||
// element where all of the "leaf" values are unknown, since that's
|
||||
// what HCL's dynamic block extension generates if the for_each
|
||||
// expression is itself unknown and thus it cannot predict how many
|
||||
// child blocks will get created.
|
||||
switch blockS.Nesting {
|
||||
case configschema.NestingSingle:
|
||||
if allLeafValuesUnknown(plannedV) && !plannedV.IsNull() {
|
||||
return errs
|
||||
}
|
||||
case configschema.NestingList, configschema.NestingMap, configschema.NestingSet:
|
||||
if plannedV.LengthInt() == 1 {
|
||||
elemVs := plannedV.AsValueSlice()
|
||||
if allLeafValuesUnknown(elemVs[0]) {
|
||||
return errs
|
||||
}
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported nesting mode %s", blockS.Nesting))
|
||||
}
|
||||
|
||||
path := append(path, cty.GetAttrStep{Name: name})
|
||||
switch blockS.Nesting {
|
||||
case configschema.NestingSingle:
|
||||
moreErrs := assertObjectCompatible(&blockS.Block, plannedV, actualV, path)
|
||||
errs = append(errs, moreErrs...)
|
||||
case configschema.NestingList:
|
||||
plannedL := plannedV.LengthInt()
|
||||
actualL := actualV.LengthInt()
|
||||
if plannedL != actualL {
|
||||
errs = append(errs, path.NewErrorf("block count changed from %d to %d", plannedL, actualL))
|
||||
continue
|
||||
}
|
||||
case configschema.NestingMap:
|
||||
plannedAtys := plannedV.Type().AttributeTypes()
|
||||
actualAtys := actualV.Type().AttributeTypes()
|
||||
for k := range plannedAtys {
|
||||
if _, ok := actualAtys[k]; !ok {
|
||||
errs = append(errs, path.NewErrorf("block key %q has vanished", k))
|
||||
continue
|
||||
}
|
||||
|
||||
plannedEV := plannedV.GetAttr(k)
|
||||
actualEV := actualV.GetAttr(k)
|
||||
moreErrs := assertObjectCompatible(&blockS.Block, plannedEV, actualEV, append(path, cty.GetAttrStep{Name: k}))
|
||||
errs = append(errs, moreErrs...)
|
||||
}
|
||||
for k := range actualAtys {
|
||||
if _, ok := plannedAtys[k]; !ok {
|
||||
errs = append(errs, path.NewErrorf("new block key %q has appeared", k))
|
||||
continue
|
||||
}
|
||||
}
|
||||
case configschema.NestingSet:
|
||||
// We can't do any reasonable matching of set elements since their
|
||||
// content is also their key, and so we have no way to correlate
|
||||
// them. Because of this, we simply verify that we still have the
|
||||
// same number of elements.
|
||||
plannedL := plannedV.LengthInt()
|
||||
actualL := actualV.LengthInt()
|
||||
if plannedL < actualL {
|
||||
errs = append(errs, path.NewErrorf("block set length changed from %d to %d", plannedL, actualL))
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported nesting mode %s", blockS.Nesting))
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func assertValueCompatible(planned, actual cty.Value, path cty.Path) []error {
|
||||
// NOTE: We don't normally use the GoString rendering of cty.Value in
|
||||
// user-facing error messages as a rule, but we make an exception
|
||||
// for this function because we expect the user to pass this message on
|
||||
// verbatim to the provider development team and so more detail is better.
|
||||
|
||||
var errs []error
|
||||
if planned.Type() == cty.DynamicPseudoType {
|
||||
// Anything goes, then
|
||||
return errs
|
||||
}
|
||||
if problems := planned.Type().TestConformance(actual.Type()); len(problems) > 0 {
|
||||
errs = append(errs, path.NewErrorf("wrong final value type: %s", convert.MismatchMessage(actual.Type(), planned.Type())))
|
||||
// If the types don't match then we can't do any other comparisons,
|
||||
// so we bail early.
|
||||
return errs
|
||||
}
|
||||
|
||||
ty := planned.Type()
|
||||
switch {
|
||||
|
||||
case ty == cty.DynamicPseudoType || !planned.IsKnown():
|
||||
// We didn't know what were going to end up with during plan, so
|
||||
// anything goes during apply.
|
||||
return errs
|
||||
|
||||
case !actual.IsKnown():
|
||||
errs = append(errs, path.NewErrorf("was known, but now unknown"))
|
||||
|
||||
case ty.IsPrimitiveType():
|
||||
if !actual.Equals(planned).True() {
|
||||
errs = append(errs, path.NewErrorf("was %#v, but now %#v", planned, actual))
|
||||
}
|
||||
|
||||
case ty.IsListType() || ty.IsMapType() || ty.IsTupleType():
|
||||
for it := planned.ElementIterator(); it.Next(); {
|
||||
k, plannedV := it.Element()
|
||||
if !actual.HasIndex(k).True() {
|
||||
errs = append(errs, path.NewErrorf("element %s has vanished", indexStrForErrors(k)))
|
||||
continue
|
||||
}
|
||||
|
||||
actualV := actual.Index(k)
|
||||
moreErrs := assertValueCompatible(plannedV, actualV, append(path, cty.IndexStep{Key: k}))
|
||||
errs = append(errs, moreErrs...)
|
||||
}
|
||||
|
||||
for it := actual.ElementIterator(); it.Next(); {
|
||||
k, _ := it.Element()
|
||||
if !planned.HasIndex(k).True() {
|
||||
errs = append(errs, path.NewErrorf("new element %s has appeared", indexStrForErrors(k)))
|
||||
}
|
||||
}
|
||||
|
||||
case ty.IsObjectType():
|
||||
atys := ty.AttributeTypes()
|
||||
for name := range atys {
|
||||
// Because we already tested that the two values have the same type,
|
||||
// we can assume that the same attributes are present in both and
|
||||
// focus just on testing their values.
|
||||
plannedV := planned.GetAttr(name)
|
||||
actualV := actual.GetAttr(name)
|
||||
moreErrs := assertValueCompatible(plannedV, actualV, append(path, cty.GetAttrStep{Name: name}))
|
||||
errs = append(errs, moreErrs...)
|
||||
}
|
||||
|
||||
case ty.IsSetType():
|
||||
// We can't really do anything useful for sets here because changing
|
||||
// an unknown element to known changes the identity of the element, and
|
||||
// so we can't correlate them properly. However, we will at least check
|
||||
// to ensure that the number of elements is consistent, along with
|
||||
// the general type-match checks we ran earlier in this function.
|
||||
plannedL := planned.LengthInt()
|
||||
actualL := actual.LengthInt()
|
||||
if plannedL < actualL {
|
||||
errs = append(errs, path.NewErrorf("length changed from %d to %d", plannedL, actualL))
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func indexStrForErrors(v cty.Value) string {
|
||||
switch v.Type() {
|
||||
case cty.Number:
|
||||
return v.AsBigFloat().Text('f', -1)
|
||||
case cty.String:
|
||||
return strconv.Quote(v.AsString())
|
||||
default:
|
||||
// Should be impossible, since no other index types are allowed!
|
||||
return fmt.Sprintf("%#v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func allLeafValuesUnknown(v cty.Value) bool {
|
||||
seenKnownValue := false
|
||||
cty.Walk(v, func(path cty.Path, cv cty.Value) (bool, error) {
|
||||
if cv.IsNull() {
|
||||
seenKnownValue = true
|
||||
}
|
||||
if cv.Type().IsPrimitiveType() && cv.IsKnown() {
|
||||
seenKnownValue = true
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
return !seenKnownValue
|
||||
}
|
|
@ -0,0 +1,878 @@
|
|||
package objchange
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/apparentlymart/go-dump/dump"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/hashicorp/terraform/configs/configschema"
|
||||
"github.com/hashicorp/terraform/tfdiags"
|
||||
)
|
||||
|
||||
func TestAssertObjectCompatible(t *testing.T) {
|
||||
tests := []struct {
|
||||
Schema *configschema.Block
|
||||
Planned cty.Value
|
||||
Actual cty.Value
|
||||
WantErrs []string
|
||||
}{
|
||||
{
|
||||
&configschema.Block{},
|
||||
cty.EmptyObjectVal,
|
||||
cty.EmptyObjectVal,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"name": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"name": cty.StringVal("thingy"),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"name": cty.StringVal("thingy"),
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"name": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"name": cty.UnknownVal(cty.String),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"name": cty.StringVal("thingy"),
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"name": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"name": cty.StringVal("wotsit"),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"name": cty.StringVal("thingy"),
|
||||
}),
|
||||
[]string{
|
||||
`.name: was cty.StringVal("wotsit"), but now cty.StringVal("thingy")`,
|
||||
},
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"stuff": {
|
||||
Type: cty.DynamicPseudoType,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"stuff": cty.DynamicVal,
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"stuff": cty.StringVal("thingy"),
|
||||
}),
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"stuff": {
|
||||
Type: cty.DynamicPseudoType,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"stuff": cty.StringVal("wotsit"),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"stuff": cty.StringVal("thingy"),
|
||||
}),
|
||||
[]string{
|
||||
`.stuff: was cty.StringVal("wotsit"), but now cty.StringVal("thingy")`,
|
||||
},
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"stuff": {
|
||||
Type: cty.DynamicPseudoType,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"stuff": cty.StringVal("true"),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"stuff": cty.True,
|
||||
}),
|
||||
[]string{
|
||||
`.stuff: wrong final value type: string required`,
|
||||
},
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"stuff": {
|
||||
Type: cty.DynamicPseudoType,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"stuff": cty.DynamicVal,
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"stuff": cty.EmptyObjectVal,
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"stuff": {
|
||||
Type: cty.DynamicPseudoType,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"stuff": cty.ObjectVal(map[string]cty.Value{
|
||||
"nonsense": cty.StringVal("yup"),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"stuff": cty.EmptyObjectVal,
|
||||
}),
|
||||
[]string{
|
||||
`.stuff: wrong final value type: attribute "nonsense" is required`,
|
||||
},
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"tags": {
|
||||
Type: cty.Map(cty.String),
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"tags": cty.MapVal(map[string]cty.Value{
|
||||
"Name": cty.StringVal("thingy"),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"tags": cty.MapVal(map[string]cty.Value{
|
||||
"Name": cty.StringVal("thingy"),
|
||||
}),
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"tags": {
|
||||
Type: cty.Map(cty.String),
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"tags": cty.MapVal(map[string]cty.Value{
|
||||
"Name": cty.UnknownVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"tags": cty.MapVal(map[string]cty.Value{
|
||||
"Name": cty.StringVal("thingy"),
|
||||
}),
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"tags": {
|
||||
Type: cty.Map(cty.String),
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"tags": cty.MapVal(map[string]cty.Value{
|
||||
"Name": cty.StringVal("wotsit"),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"tags": cty.MapVal(map[string]cty.Value{
|
||||
"Name": cty.StringVal("thingy"),
|
||||
}),
|
||||
}),
|
||||
[]string{
|
||||
`.tags["Name"]: was cty.StringVal("wotsit"), but now cty.StringVal("thingy")`,
|
||||
},
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"tags": {
|
||||
Type: cty.Map(cty.String),
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"tags": cty.MapVal(map[string]cty.Value{
|
||||
"Name": cty.StringVal("thingy"),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"tags": cty.MapVal(map[string]cty.Value{
|
||||
"Name": cty.StringVal("thingy"),
|
||||
"Env": cty.StringVal("production"),
|
||||
}),
|
||||
}),
|
||||
[]string{
|
||||
`.tags: new element "Env" has appeared`,
|
||||
},
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"tags": {
|
||||
Type: cty.Map(cty.String),
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"tags": cty.MapVal(map[string]cty.Value{
|
||||
"Name": cty.StringVal("thingy"),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"tags": cty.MapValEmpty(cty.String),
|
||||
}),
|
||||
[]string{
|
||||
`.tags: element "Name" has vanished`,
|
||||
},
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"tags": {
|
||||
Type: cty.Map(cty.String),
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"tags": cty.MapVal(map[string]cty.Value{
|
||||
"Name": cty.UnknownVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"tags": cty.MapVal(map[string]cty.Value{
|
||||
"Name": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"zones": {
|
||||
Type: cty.Set(cty.String),
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"zones": cty.SetVal([]cty.Value{
|
||||
cty.StringVal("thingy"),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"zones": cty.SetVal([]cty.Value{
|
||||
cty.StringVal("thingy"),
|
||||
}),
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"zones": {
|
||||
Type: cty.Set(cty.String),
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"zones": cty.SetVal([]cty.Value{
|
||||
cty.StringVal("thingy"),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"zones": cty.SetVal([]cty.Value{
|
||||
cty.StringVal("thingy"),
|
||||
cty.StringVal("wotsit"),
|
||||
}),
|
||||
}),
|
||||
[]string{
|
||||
`.zones: length changed from 1 to 2`,
|
||||
},
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"zones": {
|
||||
Type: cty.Set(cty.String),
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"zones": cty.SetVal([]cty.Value{
|
||||
cty.UnknownVal(cty.String),
|
||||
cty.UnknownVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"zones": cty.SetVal([]cty.Value{
|
||||
// Imagine that both of our unknown values ultimately resolved to "thingy",
|
||||
// causing them to collapse into a single element. That's valid,
|
||||
// even though it's also a little confusing and counter-intuitive.
|
||||
cty.StringVal("thingy"),
|
||||
}),
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"names": {
|
||||
Type: cty.List(cty.String),
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"names": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("thingy"),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"names": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("thingy"),
|
||||
}),
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"names": {
|
||||
Type: cty.List(cty.String),
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"names": cty.UnknownVal(cty.List(cty.String)),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"names": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("thingy"),
|
||||
}),
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"names": {
|
||||
Type: cty.List(cty.String),
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"names": cty.ListVal([]cty.Value{
|
||||
cty.UnknownVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"names": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("thingy"),
|
||||
}),
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"names": {
|
||||
Type: cty.List(cty.String),
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"names": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("thingy"),
|
||||
cty.UnknownVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"names": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("thingy"),
|
||||
cty.StringVal("wotsit"),
|
||||
}),
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"names": {
|
||||
Type: cty.List(cty.String),
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"names": cty.ListVal([]cty.Value{
|
||||
cty.UnknownVal(cty.String),
|
||||
cty.StringVal("thingy"),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"names": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("thingy"),
|
||||
cty.StringVal("wotsit"),
|
||||
}),
|
||||
}),
|
||||
[]string{
|
||||
`.names[1]: was cty.StringVal("thingy"), but now cty.StringVal("wotsit")`,
|
||||
},
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"names": {
|
||||
Type: cty.List(cty.String),
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"names": cty.ListVal([]cty.Value{
|
||||
cty.UnknownVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"names": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("thingy"),
|
||||
cty.StringVal("wotsit"),
|
||||
}),
|
||||
}),
|
||||
[]string{
|
||||
`.names: new element 1 has appeared`,
|
||||
},
|
||||
},
|
||||
|
||||
// NestingSingle blocks
|
||||
{
|
||||
&configschema.Block{
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"key": {
|
||||
Nesting: configschema.NestingSingle,
|
||||
Block: configschema.Block{},
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"key": cty.EmptyObjectVal,
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"key": cty.EmptyObjectVal,
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"key": {
|
||||
Nesting: configschema.NestingSingle,
|
||||
Block: configschema.Block{},
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"key": cty.UnknownVal(cty.EmptyObject),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"key": cty.EmptyObjectVal,
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"key": {
|
||||
Nesting: configschema.NestingSingle,
|
||||
Block: configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"foo": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"key": cty.NullVal(cty.Object(map[string]cty.Type{
|
||||
"foo": cty.String,
|
||||
})),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"key": cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.StringVal("hello"),
|
||||
}),
|
||||
}),
|
||||
[]string{
|
||||
`.key: was absent, but now present`,
|
||||
},
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"key": {
|
||||
Nesting: configschema.NestingSingle,
|
||||
Block: configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"foo": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"key": cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.StringVal("hello"),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"key": cty.NullVal(cty.Object(map[string]cty.Type{
|
||||
"foo": cty.String,
|
||||
})),
|
||||
}),
|
||||
[]string{
|
||||
`.key: was present, but now absent`,
|
||||
},
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"key": {
|
||||
Nesting: configschema.NestingSingle,
|
||||
Block: configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"foo": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"key": cty.ObjectVal(map[string]cty.Value{
|
||||
// One wholly unknown block is what "dynamic" blocks
|
||||
// generate when the for_each expression is unknown.
|
||||
"foo": cty.UnknownVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"key": cty.NullVal(cty.Object(map[string]cty.Type{
|
||||
"foo": cty.String,
|
||||
})),
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
|
||||
// NestingList blocks
|
||||
{
|
||||
&configschema.Block{
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"key": {
|
||||
Nesting: configschema.NestingList,
|
||||
Block: configschema.Block{},
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"key": cty.ListVal([]cty.Value{
|
||||
cty.EmptyObjectVal,
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"key": cty.ListVal([]cty.Value{
|
||||
cty.EmptyObjectVal,
|
||||
}),
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"key": {
|
||||
Nesting: configschema.NestingList,
|
||||
Block: configschema.Block{},
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"key": cty.TupleVal([]cty.Value{
|
||||
cty.EmptyObjectVal,
|
||||
cty.EmptyObjectVal,
|
||||
}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"key": cty.TupleVal([]cty.Value{
|
||||
cty.EmptyObjectVal,
|
||||
}),
|
||||
}),
|
||||
[]string{
|
||||
`.key: block count changed from 2 to 1`,
|
||||
},
|
||||
},
|
||||
{
|
||||
&configschema.Block{
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"key": {
|
||||
Nesting: configschema.NestingList,
|
||||
Block: configschema.Block{},
|
||||
},
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"key": cty.TupleVal([]cty.Value{}),
|
||||
}),
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.UnknownVal(cty.String),
|
||||
"key": cty.TupleVal([]cty.Value{
|
||||
cty.EmptyObjectVal,
|
||||
cty.EmptyObjectVal,
|
||||
}),
|
||||
}),
|
||||
[]string{
|
||||
`.key: block count changed from 0 to 2`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%#v and %#v", test.Planned, test.Actual), func(t *testing.T) {
|
||||
errs := AssertObjectCompatible(test.Schema, test.Planned, test.Actual)
|
||||
|
||||
wantErrs := make(map[string]struct{})
|
||||
gotErrs := make(map[string]struct{})
|
||||
for _, err := range errs {
|
||||
gotErrs[tfdiags.FormatError(err)] = struct{}{}
|
||||
}
|
||||
for _, msg := range test.WantErrs {
|
||||
wantErrs[msg] = struct{}{}
|
||||
}
|
||||
|
||||
t.Logf("\nplanned: %sactual: %s", dump.Value(test.Planned), dump.Value(test.Actual))
|
||||
for msg := range wantErrs {
|
||||
if _, ok := gotErrs[msg]; !ok {
|
||||
t.Errorf("missing expected error: %s", msg)
|
||||
}
|
||||
}
|
||||
for msg := range gotErrs {
|
||||
if _, ok := wantErrs[msg]; !ok {
|
||||
t.Errorf("unexpected extra error: %s", msg)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue