| 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) |
| } |
| } |
| }) |
| } |
| } |