blob: 60c9876702eeae66f8e387678f36d4585e2c9656 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package objchange
import (
"fmt"
"testing"
"github.com/apparentlymart/go-dump/dump"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/hashicorp/terraform/internal/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(marks.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(marks.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(marks.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(marks.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)
}
}
})
}
}