1376 lines
32 KiB
Go
1376 lines
32 KiB
Go
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) {
|
|
schemaWithFoo := configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"foo": {Type: cty.String, Optional: true},
|
|
},
|
|
}
|
|
fooBlockValue := cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("bar"),
|
|
})
|
|
schemaWithFooBar := configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"foo": {Type: cty.String, Optional: true},
|
|
"bar": {Type: cty.String, Optional: true},
|
|
},
|
|
}
|
|
fooBarBlockValue := cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("bar"),
|
|
"bar": cty.NullVal(cty.String), // simulating the situation where bar isn't set in the config at all
|
|
})
|
|
|
|
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,
|
|
},
|
|
"name": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
Sensitive: 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: inconsistent values for sensitive attribute`,
|
|
},
|
|
},
|
|
{
|
|
&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").Mark("sensitive"),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.UnknownVal(cty.String),
|
|
"name": cty.StringVal("thingy"),
|
|
}),
|
|
[]string{
|
|
`.name: inconsistent values for sensitive attribute`,
|
|
},
|
|
},
|
|
{
|
|
&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").Mark("sensitive"),
|
|
}),
|
|
[]string{
|
|
`.name: inconsistent values for sensitive attribute`,
|
|
},
|
|
},
|
|
{
|
|
// This tests the codepath that leads to couldHaveUnknownBlockPlaceholder,
|
|
// where a set may be sensitive and need to be unmarked before it
|
|
// is iterated upon
|
|
&configschema.Block{
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"configuration": {
|
|
Nesting: configschema.NestingList,
|
|
Block: configschema.Block{
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"sensitive_fields": {
|
|
Nesting: configschema.NestingSet,
|
|
Block: schemaWithFoo,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"configuration": cty.TupleVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"sensitive_fields": cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("secret"),
|
|
}),
|
|
}).Mark("sensitive"),
|
|
}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"configuration": cty.TupleVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"sensitive_fields": cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("secret"),
|
|
}),
|
|
}).Mark("sensitive"),
|
|
}),
|
|
}),
|
|
}),
|
|
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.DynamicVal,
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"id": cty.UnknownVal(cty.String),
|
|
"stuff": cty.StringVal("thingy"),
|
|
}),
|
|
[]string{},
|
|
},
|
|
{
|
|
&configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"obj": {
|
|
Type: cty.Object(map[string]cty.Type{
|
|
"stuff": cty.DynamicPseudoType,
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"obj": cty.ObjectVal(map[string]cty.Value{
|
|
"stuff": cty.DynamicVal,
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"obj": cty.ObjectVal(map[string]cty.Value{
|
|
"stuff": cty.NumberIntVal(3),
|
|
}),
|
|
}),
|
|
[]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: actual set element cty.StringVal("wotsit") does not correlate with any element in plan`,
|
|
`.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{
|
|
"key": cty.EmptyObjectVal,
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"key": cty.EmptyObjectVal,
|
|
}),
|
|
nil,
|
|
},
|
|
{
|
|
&configschema.Block{
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"key": {
|
|
Nesting: configschema.NestingSingle,
|
|
Block: configschema.Block{},
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"key": cty.UnknownVal(cty.EmptyObject),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"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.UnknownVal(cty.Object(map[string]cty.Type{
|
|
"key": cty.Object(map[string]cty.Type{
|
|
"foo": 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: schemaWithFoo,
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"key": cty.ListVal([]cty.Value{
|
|
fooBlockValue,
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"key": cty.ListVal([]cty.Value{
|
|
fooBlockValue,
|
|
}),
|
|
}),
|
|
nil,
|
|
},
|
|
{
|
|
&configschema.Block{
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"key": {
|
|
Nesting: configschema.NestingList,
|
|
Block: schemaWithFoo,
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"key": cty.TupleVal([]cty.Value{
|
|
fooBlockValue,
|
|
fooBlockValue,
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"key": cty.TupleVal([]cty.Value{
|
|
fooBlockValue,
|
|
}),
|
|
}),
|
|
[]string{
|
|
`.key: block count changed from 2 to 1`,
|
|
},
|
|
},
|
|
{
|
|
&configschema.Block{
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"key": {
|
|
Nesting: configschema.NestingList,
|
|
Block: schemaWithFoo,
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"key": cty.TupleVal([]cty.Value{}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"key": cty.TupleVal([]cty.Value{
|
|
fooBlockValue,
|
|
fooBlockValue,
|
|
}),
|
|
}),
|
|
[]string{
|
|
`.key: block count changed from 0 to 2`,
|
|
},
|
|
},
|
|
{
|
|
&configschema.Block{
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"key": {
|
|
Nesting: configschema.NestingList,
|
|
Block: schemaWithFooBar,
|
|
},
|
|
},
|
|
},
|
|
cty.UnknownVal(cty.Object(map[string]cty.Type{
|
|
"key": cty.List(fooBarBlockValue.Type()),
|
|
})),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"key": cty.ListVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("hello"),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("world"),
|
|
}),
|
|
}),
|
|
}),
|
|
nil, // an unknown block is allowed to expand into multiple, because that's how dynamic blocks behave when for_each is unknown
|
|
},
|
|
{
|
|
&configschema.Block{
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"key": {
|
|
Nesting: configschema.NestingList,
|
|
Block: schemaWithFooBar,
|
|
},
|
|
},
|
|
},
|
|
// While we must make an exception for empty strings in sets due to
|
|
// the legacy SDK, lists should be compared more strictly.
|
|
// This does not count as a dynamic block placeholder
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"key": cty.ListVal([]cty.Value{
|
|
fooBarBlockValue,
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.UnknownVal(cty.String),
|
|
"bar": cty.StringVal(""),
|
|
}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"key": cty.ListVal([]cty.Value{
|
|
fooBlockValue,
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("hello"),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("world"),
|
|
}),
|
|
}),
|
|
}),
|
|
[]string{".key: block count changed from 2 to 3"},
|
|
},
|
|
|
|
// NestingSet blocks
|
|
{
|
|
&configschema.Block{
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"block": {
|
|
Nesting: configschema.NestingSet,
|
|
Block: schemaWithFoo,
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"block": cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("hello"),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("world"),
|
|
}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"block": cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("hello"),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("world"),
|
|
}),
|
|
}),
|
|
}),
|
|
nil,
|
|
},
|
|
{
|
|
&configschema.Block{
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"block": {
|
|
Nesting: configschema.NestingSet,
|
|
Block: schemaWithFoo,
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"block": cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.UnknownVal(cty.String),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.UnknownVal(cty.String),
|
|
}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"block": cty.SetVal([]cty.Value{
|
|
// This is testing the scenario where the two unknown values
|
|
// turned out to be equal after we learned their values,
|
|
// and so they coalesced together into a single element.
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("hello"),
|
|
}),
|
|
}),
|
|
}),
|
|
nil,
|
|
},
|
|
{
|
|
&configschema.Block{
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"block": {
|
|
Nesting: configschema.NestingSet,
|
|
Block: schemaWithFoo,
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"block": cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.UnknownVal(cty.String),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.UnknownVal(cty.String),
|
|
}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"block": cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("hello"),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("world"),
|
|
}),
|
|
}),
|
|
}),
|
|
nil,
|
|
},
|
|
{
|
|
&configschema.Block{
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"block": {
|
|
Nesting: configschema.NestingSet,
|
|
Block: schemaWithFoo,
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"block": cty.UnknownVal(cty.Set(
|
|
cty.Object(map[string]cty.Type{
|
|
"foo": cty.String,
|
|
}),
|
|
)),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"block": cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("hello"),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("world"),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("nope"),
|
|
}),
|
|
}),
|
|
}),
|
|
// there is no error here, because the presence of unknowns
|
|
// indicates this may be a dynamic block, and the length is unknown
|
|
nil,
|
|
},
|
|
{
|
|
&configschema.Block{
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"block": {
|
|
Nesting: configschema.NestingSet,
|
|
Block: schemaWithFoo,
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"block": cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("hello"),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("world"),
|
|
}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"block": cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("howdy"),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("world"),
|
|
}),
|
|
}),
|
|
}),
|
|
[]string{
|
|
`.block: planned set element cty.ObjectVal(map[string]cty.Value{"foo":cty.StringVal("hello")}) does not correlate with any element in actual`,
|
|
},
|
|
},
|
|
{
|
|
// This one is an odd situation where the value representing the
|
|
// block itself is unknown. This is never supposed to be true,
|
|
// but in legacy SDK mode we allow such things to pass through as
|
|
// a warning, and so we must tolerate them for matching purposes.
|
|
&configschema.Block{
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"block": {
|
|
Nesting: configschema.NestingSet,
|
|
Block: schemaWithFoo,
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"block": cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.UnknownVal(cty.String),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.UnknownVal(cty.String),
|
|
}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"block": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{
|
|
"foo": cty.String,
|
|
}))),
|
|
}),
|
|
nil,
|
|
},
|
|
{
|
|
&configschema.Block{
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"block": {
|
|
Nesting: configschema.NestingSet,
|
|
Block: schemaWithFoo,
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"block": cty.UnknownVal(cty.Set(fooBlockValue.Type())),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"block": cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("a"),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("b"),
|
|
}),
|
|
}),
|
|
}),
|
|
nil,
|
|
},
|
|
// test a set with an unknown dynamic count going to 0 values
|
|
{
|
|
&configschema.Block{
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"block2": {
|
|
Nesting: configschema.NestingSet,
|
|
Block: schemaWithFoo,
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"block2": cty.UnknownVal(cty.Set(fooBlockValue.Type())),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"block2": cty.SetValEmpty(cty.Object(map[string]cty.Type{
|
|
"foo": cty.String,
|
|
})),
|
|
}),
|
|
nil,
|
|
},
|
|
// test a set with a patially known dynamic count reducing it's values
|
|
{
|
|
&configschema.Block{
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"block3": {
|
|
Nesting: configschema.NestingSet,
|
|
Block: schemaWithFoo,
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"block3": cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("a"),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.UnknownVal(cty.String),
|
|
}),
|
|
}),
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"block3": cty.SetVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"foo": cty.StringVal("a"),
|
|
}),
|
|
}),
|
|
}),
|
|
nil,
|
|
},
|
|
{
|
|
&configschema.Block{
|
|
BlockTypes: map[string]*configschema.NestedBlock{
|
|
"block": {
|
|
Nesting: configschema.NestingList,
|
|
Block: configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"foo": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"block": cty.EmptyObjectVal,
|
|
}),
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"block": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{
|
|
"foo": cty.String,
|
|
}))),
|
|
}),
|
|
nil,
|
|
},
|
|
}
|
|
|
|
for i, test := range tests {
|
|
t.Run(fmt.Sprintf("%02d: %#v and %#v", i, 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)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|